diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..a847f962a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: "/backend" + open-pull-requests-limit: 100 + schedule: + interval: weekly + - package-ecosystem: npm + directory: "/frontend" + open-pull-requests-limit: 100 + schedule: + interval: weekly diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 16ebf7272..fa40a6c49 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -49,7 +49,14 @@ jobs: path: /usr/local/lib/python3.10/site-packages key: venv-${{ runner.os }}-bullseye-3.10.2-v2-${{ hashFiles('**/poetry.lock') }} - - run: python -m pytest + - run: | + python -m coverage run -m pytest + python -m coverage xml + - run: apt update && apt install -y git + - name: Code Coverage + uses: codecov/codecov-action@v2 + with: + token: ${{ secrets.CODECOV }} Lint: needs: [Test] diff --git a/README.md b/README.md index 1b8649069..8dc72edcc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # OSOC-3 +[![codecov](https://codecov.io/gh/SELab-2/OSOC-3/branch/develop/graph/badge.svg?token=gzlDMtycUN)](https://codecov.io/gh/SELab-2/OSOC-3) + ## Table of Contents [User manual](#user-manual) diff --git a/backend/.env.example b/backend/.env.example index eb6a08b2d..8fce80c40 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -8,9 +8,13 @@ DB_PORT=3306 # Can be generated using "openssl rand -hex 32" SECRET_KEY=4d16a9cc83d74144322e893c879b5f639088c15dc1606b11226abbd7e97f5ee5 # The ACCESS JWT token should be valid for ... -ACCESS_TOKEN_EXPIRE_M = 5 +ACCESS_TOKEN_EXPIRE_M=5 # The REFRESH JWT token should be valid for ... -REFRESH_TOKEN_EXPIRE_M = 2880 +REFRESH_TOKEN_EXPIRE_M=2880 + +# GitHub OAuth +GITHUB_CLIENT_ID=25 +GITHUB_CLIENT_SECRET=herebegithubclientsecret # Frontend FRONTEND_URL="http://localhost:3000" \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore index 996e38fcc..eaaf3cd68 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -160,3 +160,4 @@ cython_debug/ # Testing SQLite database *test.db +/fill_my_database.py diff --git a/backend/create_admin.py b/backend/create_admin.py index b2aff3d49..4c92dbc00 100644 --- a/backend/create_admin.py +++ b/backend/create_admin.py @@ -7,6 +7,7 @@ fiddle in the database themselves. """ import argparse +import asyncio import getpass import sys import traceback @@ -43,25 +44,26 @@ def get_hashed_password() -> str: return get_password_hash(first_pass) -def create_admin(name: str, email: str, pw: str): +async def create_admin(name: str, email: str, pw: str): """Create a new user in the database""" - with DBSession.begin() as session: + async with DBSession() as session: try: - transaction = session.begin_nested() - user = create_user(session, name, commit=False) + transaction = await session.begin_nested() + user = await create_user(session, name, commit=False) user.admin = True # Add an email auth entry - create_auth_email(session, user, pw, email, commit=False) + await create_auth_email(session, user, pw, email, commit=False) + await session.commit() except sqlalchemy.exc.SQLAlchemyError: # Something went wrong: rollback the transaction & print the error - transaction.rollback() + await transaction.rollback() # Print the traceback of the exception print(traceback.format_exc()) exit(3) - session.close() + await session.close() if __name__ == "__main__": @@ -83,6 +85,8 @@ def create_admin(name: str, email: str, pw: str): pw_hash = get_hashed_password() # Create new database entry - create_admin(args.name, args.email, pw_hash) + loop = asyncio.new_event_loop() + loop.run_until_complete(create_admin(args.name, args.email, pw_hash)) + loop.close() print("Addition successful.") diff --git a/backend/create_webhook.py b/backend/create_webhook.py new file mode 100644 index 000000000..7612896b7 --- /dev/null +++ b/backend/create_webhook.py @@ -0,0 +1,36 @@ +import asyncio +import traceback +from argparse import ArgumentParser + +import sqlalchemy.exc +from sqlalchemy.ext.asyncio import AsyncSession + +import src.app # Important import - avoids circular import +from src.database.crud import editions +from src.database.crud.webhooks import create_webhook +from src.database.engine import DBSession +from src.database.models import Edition, WebhookURL +from settings import FRONTEND_URL + + +async def do(args): + session: AsyncSession + async with DBSession() as session: + try: + edition: Edition = await editions.get_edition_by_name(session, args.edition) + webhook_url: WebhookURL = await create_webhook(session, edition) + print(f'WebhookURL created: {FRONTEND_URL}/editions/{edition.name}/webhooks/{webhook_url.uuid}') + except sqlalchemy.exc.SQLAlchemyError: + await session.rollback() + print(traceback.format_exc()) + exit(3) + + +if __name__ == '__main__': + parser = ArgumentParser(description="Add new webhook to edition.") + parser.add_argument("-E", "--edition", type=str, required=True) + args = parser.parse_args() + + loop = asyncio.new_event_loop() + loop.run_until_complete(do(args)) + loop.close() diff --git a/backend/migrations/env.py b/backend/migrations/env.py index 227fbd3ee..5c83434af 100644 --- a/backend/migrations/env.py +++ b/backend/migrations/env.py @@ -1,3 +1,4 @@ +import asyncio from logging.config import fileConfig from alembic import context @@ -21,6 +22,7 @@ # target_metadata = mymodel.Base.metadata target_metadata = Base.metadata + # other values from the config, defined by the needs of env.py, # can be acquired: # my_important_option = config.get_main_option("my_important_option") @@ -52,32 +54,33 @@ def run_migrations_offline(): context.run_migrations() -def run_migrations_online(): +def do_run_migrations(connection): + context.configure( + connection=connection, target_metadata=target_metadata, + render_as_batch=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_migrations_online(): """Run migrations in 'online' mode. In this scenario we need to create an Engine and associate a connection with the context. """ - #connectable = engine_from_config( - # config.get_section(config.config_ini_section), - # prefix="sqlalchemy.", - # poolclass=pool.NullPool, - #) connectable = engine - with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata, - render_as_batch=True - ) + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) - with context.begin_transaction(): - context.run_migrations() + await connectable.dispose() if context.is_offline_mode(): run_migrations_offline() else: - run_migrations_online() + asyncio.run(run_migrations_online()) diff --git a/backend/migrations/versions/145816a4b2df_add_form_tables.py b/backend/migrations/versions/145816a4b2df_add_form_tables.py deleted file mode 100644 index f59e75cd8..000000000 --- a/backend/migrations/versions/145816a4b2df_add_form_tables.py +++ /dev/null @@ -1,104 +0,0 @@ -"""Add Form tables - -Revision ID: 145816a4b2df -Revises: 810c6967f5d5 -Create Date: 2022-03-07 23:29:05.003945 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql -import sqlalchemy_utils - -# revision identifiers, used by Alembic. -revision = '145816a4b2df' -down_revision = '810c6967f5d5' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - 'webhook_urls', - sa.Column('webhook_id', sa.Integer(), nullable=False), - sa.Column('uuid', sqlalchemy_utils.types.uuid.UUIDType(binary=False), nullable=True), - sa.Column('edition_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['edition_id'], ['editions.edition_id'], name='webhook_urls_editions_fk'), - sa.PrimaryKeyConstraint('webhook_id') - ) - op.create_table( - 'questions', - sa.Column('question_id', sa.Integer(), nullable=False), - sa.Column('type', sa.Enum( - 'CHECKBOXES', 'FILE_UPLOAD', 'INPUT_EMAIL', 'INPUT_LINK', 'INPUT_PHONE_NUMBER', - 'INPUT_TEXT', 'MULTIPLE_CHOICE', 'TEXTAREA', 'INPUT_NUMBER', name='questionenum' - ), nullable=False), - sa.Column('question', sa.Text(), nullable=False), - sa.Column('student_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['student_id'], ['students.student_id'], name='questions_students_fk'), - sa.PrimaryKeyConstraint('question_id') - ) - op.create_table( - 'question_answers', - sa.Column('answer_id', sa.Integer(), nullable=False), - sa.Column('answer', sa.Text(), nullable=True), - sa.Column('question_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['question_id'], ['questions.question_id'], name='question_answers_questions_fk'), - sa.PrimaryKeyConstraint('answer_id') - ) - op.create_table( - 'question_file_answers', - sa.Column('file_answer_id', sa.Integer(), nullable=False), - sa.Column('file_name', sa.Text(), nullable=False), - sa.Column('url', sa.Text(), nullable=False), - sa.Column('mime_type', sa.Text(), nullable=False), - sa.Column('size', sa.Integer(), nullable=False), - sa.Column('question_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['question_id'], ['questions.question_id'], name='question_file_answers_questions_fk'), - sa.PrimaryKeyConstraint('file_answer_id') - ) - - with op.batch_alter_table('students', schema=None) as batch_op: - batch_op.add_column(sa.Column('first_name', sa.Text(), nullable=False)) - batch_op.add_column(sa.Column('last_name', sa.Text(), nullable=False)) - batch_op.add_column(sa.Column('preferred_name', sa.Text(), nullable=True)) - batch_op.add_column(sa.Column('edition_id', sa.Integer(), nullable=True)) - batch_op.drop_constraint('students_webhooks_fk', type_='foreignkey') - batch_op.create_foreign_key('students_editions_fk', 'editions', ['edition_id'], ['edition_id']) - batch_op.drop_column('cv_webhook_id') - batch_op.drop_column('name') - - op.drop_table('webhooks') - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - 'webhooks', - sa.Column('webhook_id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False), - sa.Column('edition_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['edition_id'], ['editions.edition_id'], name='webhooks_editions_fk'), - sa.PrimaryKeyConstraint('webhook_id'), - mariadb_default_charset='latin1', - mariadb_engine='InnoDB' - ) - - with op.batch_alter_table('students', schema=None) as batch_op: - batch_op.add_column(sa.Column('name', mysql.TEXT(), nullable=False)) - batch_op.add_column( - sa.Column('cv_webhook_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True)) - batch_op.drop_constraint('students_editions_fk', type_='foreignkey') - batch_op.create_foreign_key('students_webhooks_fk', 'webhooks', ['cv_webhook_id'], ['webhook_id']) - batch_op.drop_column('edition_id') - batch_op.drop_column('preferred_name') - batch_op.drop_column('last_name') - batch_op.drop_column('first_name') - - op.drop_table('question_file_answers') - op.drop_table('question_answers') - op.drop_table('questions') - op.drop_table('webhook_urls') - # ### end Alembic commands ### diff --git a/backend/migrations/versions/1862d7dea4cc_.py b/backend/migrations/versions/1862d7dea4cc_.py deleted file mode 100644 index 094e930eb..000000000 --- a/backend/migrations/versions/1862d7dea4cc_.py +++ /dev/null @@ -1,24 +0,0 @@ -"""empty message - -Revision ID: 1862d7dea4cc -Revises: 8c97ecc58e5f, a4a047b881db -Create Date: 2022-04-08 16:05:01.649808 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '1862d7dea4cc' -down_revision = ('8c97ecc58e5f', 'a4a047b881db') -branch_labels = None -depends_on = None - - -def upgrade(): - pass - - -def downgrade(): - pass diff --git a/backend/migrations/versions/43e6e98fe039_create_enum_for_email_statuses.py b/backend/migrations/versions/43e6e98fe039_create_enum_for_email_statuses.py deleted file mode 100644 index f51d5c0aa..000000000 --- a/backend/migrations/versions/43e6e98fe039_create_enum_for_email_statuses.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Create enum for email statuses - -Revision ID: 43e6e98fe039 -Revises: a4a047b881db -Create Date: 2022-04-13 16:24:26.687617 - -""" -from alembic import op -import sqlalchemy as sa - -# revision identifiers, used by Alembic. -revision = '43e6e98fe039' -down_revision = 'a4a047b881db' -branch_labels = None -depends_on = None - -new_type = sa.Enum('APPLIED', 'AWAITING_PROJECT', 'APPROVED', 'CONTRACT_CONFIRMED', 'CONTRACT_DECLINED', 'REJECTED', - name='emailstatusenum') -old_type = sa.Enum('UNDECIDED', 'YES', 'MAYBE', 'NO', name='decisionenum') - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table("decision_emails", schema=None) as batch_op: - batch_op.alter_column("decision", type_=new_type, existing_type=old_type, nullable=False) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table("decision_emails", schema=None) as batch_op: - batch_op.alter_column("decision", type_=old_type, existing_type=new_type, nullable=False) - # ### end Alembic commands ### diff --git a/backend/migrations/versions/6433759fff33_initial_setup.py b/backend/migrations/versions/55e311757afa_setup_database.py similarity index 58% rename from backend/migrations/versions/6433759fff33_initial_setup.py rename to backend/migrations/versions/55e311757afa_setup_database.py index 0ea27b4bc..116cc1b05 100644 --- a/backend/migrations/versions/6433759fff33_initial_setup.py +++ b/backend/migrations/versions/55e311757afa_setup_database.py @@ -1,8 +1,8 @@ -"""Initial setup +"""Setup database -Revision ID: 6433759fff33 +Revision ID: 55e311757afa Revises: -Create Date: 2022-03-03 16:59:00.494493 +Create Date: 2022-05-22 13:34:31.251814 """ import sqlalchemy as sa @@ -10,26 +10,21 @@ from alembic import op # revision identifiers, used by Alembic. -revision = '6433759fff33' +revision = '55e311757afa' down_revision = None branch_labels = None depends_on = None def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### op.create_table( 'editions', sa.Column('edition_id', sa.Integer(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), sa.Column('year', sa.Integer(), nullable=False), - sa.PrimaryKeyConstraint('edition_id') - ) - op.create_table( - 'invite_links', - sa.Column('invite_link_id', sa.Integer(), nullable=False), - sa.Column('uuid', sqlalchemy_utils.types.uuid.UUIDType(binary=False), nullable=True), - sa.Column('target_email', sa.Text(), nullable=False), - sa.PrimaryKeyConstraint('invite_link_id') + sa.Column('readonly', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('edition_id'), + sa.UniqueConstraint('name') ) op.create_table( 'partners', @@ -42,21 +37,21 @@ def upgrade(): 'skills', sa.Column('skill_id', sa.Integer(), nullable=False), sa.Column('name', sa.Text(), nullable=False), - sa.Column('description', sa.Text(), nullable=True), sa.PrimaryKeyConstraint('skill_id') ) op.create_table( 'users', sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('name', sa.Text(), nullable=False), - sa.Column('email', sa.Text(), nullable=False), - sa.PrimaryKeyConstraint('user_id'), - sa.UniqueConstraint('email') + sa.Column('admin', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('user_id') ) op.create_table( 'coach_requests', sa.Column('request_id', sa.Integer(), nullable=False), sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('edition_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['edition_id'], ['editions.edition_id'], name='coach_requests_editions_fk'), sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], name='coach_requests_users_fk'), sa.PrimaryKeyConstraint('request_id') ) @@ -64,50 +59,104 @@ def upgrade(): 'email_auths', sa.Column('email_auth_id', sa.Integer(), nullable=False), sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('email', sa.Text(), nullable=False), sa.Column('pw_hash', sa.Text(), nullable=False), sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], name='email_auths_users_fk'), - sa.PrimaryKeyConstraint('email_auth_id') + sa.PrimaryKeyConstraint('email_auth_id'), + sa.UniqueConstraint('email') ) op.create_table( 'github_auths', sa.Column('gh_auth_id', sa.Integer(), nullable=False), + sa.Column('access_token', sa.Text(), nullable=True), + sa.Column('email', sa.Text(), nullable=False), + sa.Column('github_user_id', sa.Integer(), nullable=False), sa.Column('user_id', sa.Integer(), nullable=False), sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], name='github_auths_users_fk'), - sa.PrimaryKeyConstraint('gh_auth_id') + sa.PrimaryKeyConstraint('gh_auth_id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('github_user_id') ) op.create_table( 'google_auths', sa.Column('google_auth_id', sa.Integer(), nullable=False), sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('email', sa.Text(), nullable=False), sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], name='google_auths_users_fk'), - sa.PrimaryKeyConstraint('google_auth_id') + sa.PrimaryKeyConstraint('google_auth_id'), + sa.UniqueConstraint('email') + ) + op.create_table( + 'invite_links', + sa.Column('invite_link_id', sa.Integer(), nullable=False), + sa.Column('uuid', sqlalchemy_utils.types.uuid.UUIDType(binary=False), nullable=True), + sa.Column('target_email', sa.Text(), nullable=False), + sa.Column('edition_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['edition_id'], ['editions.edition_id'], name='invite_links_editions_fk'), + sa.PrimaryKeyConstraint('invite_link_id') ) op.create_table( 'projects', sa.Column('project_id', sa.Integer(), nullable=False), sa.Column('name', sa.Text(), nullable=False), - sa.Column('number_of_students', sa.Integer(), nullable=False), + sa.Column('info_url', sa.Text(), nullable=True), sa.Column('edition_id', sa.Integer(), nullable=True), sa.ForeignKeyConstraint(['edition_id'], ['editions.edition_id'], name='projects_editions_fk'), sa.PrimaryKeyConstraint('project_id') ) op.create_table( - 'user_roles', - sa.Column('user_role_id', sa.Integer(), nullable=False), + 'students', + sa.Column('student_id', sa.Integer(), nullable=False), + sa.Column('first_name', sa.Text(), nullable=False), + sa.Column('last_name', sa.Text(), nullable=False), + sa.Column('preferred_name', sa.Text(), nullable=True), + sa.Column('email_address', sa.Text(), nullable=False), + sa.Column('phone_number', sa.Text(), nullable=True), + sa.Column('alumni', sa.Boolean(), nullable=False), + sa.Column('decision', sa.Enum('UNDECIDED', 'YES', 'MAYBE', 'NO', name='decisionenum'), nullable=True), + sa.Column('wants_to_be_student_coach', sa.Boolean(), nullable=False), + sa.Column('edition_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['edition_id'], ['editions.edition_id'], name='students_editions_fk'), + sa.PrimaryKeyConstraint('student_id'), + sa.UniqueConstraint('email_address'), + sa.UniqueConstraint('phone_number') + ) + op.create_table( + 'user_editions', sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('role', sa.Enum('ADMIN', 'COACH', name='roleenum'), nullable=True), sa.Column('edition_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['edition_id'], ['editions.edition_id'], name='user_roles_editions_fk'), - sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], name='user_roles_users_fk'), - sa.PrimaryKeyConstraint('user_role_id') + sa.ForeignKeyConstraint(['edition_id'], ['editions.edition_id'], name='user_editions_editions_fk'), + sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], name='user_editions_users_fk') ) op.create_table( - 'webhooks', + 'webhook_urls', sa.Column('webhook_id', sa.Integer(), nullable=False), + sa.Column('uuid', sqlalchemy_utils.types.uuid.UUIDType(binary=False), nullable=True), sa.Column('edition_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['edition_id'], ['editions.edition_id'], name='webhooks_editions_fk'), + sa.ForeignKeyConstraint(['edition_id'], ['editions.edition_id'], name='webhook_urls_editions_fk'), sa.PrimaryKeyConstraint('webhook_id') ) + op.create_table( + 'decision_emails', + sa.Column('email_id', sa.Integer(), nullable=False), + sa.Column('student_id', sa.Integer(), nullable=False), + sa.Column( + 'decision', + sa.Enum( + 'APPLIED', + 'AWAITING_PROJECT', + 'APPROVED', + 'CONTRACT_CONFIRMED', + 'CONTRACT_DECLINED', + 'REJECTED', + name='emailstatusenum' + ), + nullable=False + ), + sa.Column('date', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['student_id'], ['students.student_id'], name='decision_emails_students_fk'), + sa.PrimaryKeyConstraint('email_id') + ) op.create_table( 'project_coaches', sa.Column('project_id', sa.Integer(), nullable=True), @@ -119,53 +168,44 @@ def upgrade(): 'project_partners', sa.Column('project_id', sa.Integer(), nullable=True), sa.Column('partner_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['partner_id'], ['partners.partner_id'], name='project_partners'), + sa.ForeignKeyConstraint(['partner_id'], ['partners.partner_id'], name='project_partners_partners_fk'), sa.ForeignKeyConstraint(['project_id'], ['projects.project_id'], name='project_partners_projects_fk') ) op.create_table( - 'project_skills', + 'project_roles', + sa.Column('project_role_id', sa.Integer(), nullable=False), sa.Column('project_id', sa.Integer(), nullable=True), sa.Column('skill_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['project_id'], ['projects.project_id'], name='project_skills_projects_fk'), - sa.ForeignKeyConstraint(['skill_id'], ['skills.skill_id'], name='project_skills_skills_fk') - ) - op.create_table( - 'students', - sa.Column('student_id', sa.Integer(), nullable=False), - sa.Column('name', sa.Text(), nullable=False), - sa.Column('email_address', sa.Text(), nullable=False), - sa.Column('phone_number', sa.Text(), nullable=True), - sa.Column('alumni', sa.Boolean(), nullable=False), - sa.Column('cv_webhook_id', sa.Integer(), nullable=True), - sa.Column('decision', sa.Enum('UNDECIDED', 'YES', 'MAYBE', 'NO', name='decisionenum'), nullable=True), - sa.Column('wants_to_be_student_coach', sa.Boolean(), nullable=False), - sa.ForeignKeyConstraint(['cv_webhook_id'], ['webhooks.webhook_id'], name='students_webhooks_fk'), - sa.PrimaryKeyConstraint('student_id'), - sa.UniqueConstraint('email_address'), - sa.UniqueConstraint('phone_number') - ) - op.create_table( - 'decision_emails', - sa.Column('email_id', sa.Integer(), nullable=False), - sa.Column('student_id', sa.Integer(), nullable=False), - sa.Column('decision', sa.Enum('UNDECIDED', 'YES', 'MAYBE', 'NO', name='decisionenum'), nullable=False), - sa.Column('date', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['student_id'], ['students.student_id'], name='decision_emails_students_fk'), - sa.PrimaryKeyConstraint('email_id') + sa.Column('description', sa.Text(), nullable=True), + sa.Column('slots', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['project_id'], ['projects.project_id'], name='project_roles_projects_fk'), + sa.ForeignKeyConstraint(['skill_id'], ['skills.skill_id'], name='project_roles_skills_fk'), + sa.PrimaryKeyConstraint('project_role_id') ) op.create_table( - 'project_roles', + 'questions', + sa.Column('question_id', sa.Integer(), nullable=False), + sa.Column( + 'type', + sa.Enum( + 'CHECKBOXES', + 'FILE_UPLOAD', + 'INPUT_EMAIL', + 'INPUT_LINK', + 'INPUT_PHONE_NUMBER', + 'INPUT_TEXT', + 'INPUT_NUMBER', + 'MULTIPLE_CHOICE', + 'TEXTAREA', + 'UNKNOWN', + name='questionenum' + ), + nullable=False + ), + sa.Column('question', sa.Text(), nullable=False), sa.Column('student_id', sa.Integer(), nullable=False), - sa.Column('project_id', sa.Integer(), nullable=False), - sa.Column('skill_id', sa.Integer(), nullable=False), - sa.Column('definitive', sa.Boolean(), nullable=False), - sa.Column('argumentation', sa.Text(), nullable=True), - sa.Column('drafter_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['drafter_id'], ['users.user_id'], name='project_roles_users_fk'), - sa.ForeignKeyConstraint(['project_id'], ['projects.project_id'], name='project_roles_projects_fk'), - sa.ForeignKeyConstraint(['skill_id'], ['skills.skill_id'], name='project_roles_skills_fk'), - sa.ForeignKeyConstraint(['student_id'], ['students.student_id'], name='project_roles_students_fk'), - sa.PrimaryKeyConstraint('student_id', 'project_id', 'skill_id') + sa.ForeignKeyConstraint(['student_id'], ['students.student_id'], name='questions_students_fk'), + sa.PrimaryKeyConstraint('question_id') ) op.create_table( 'student_skills', @@ -178,29 +218,69 @@ def upgrade(): 'suggestions', sa.Column('suggestion_id', sa.Integer(), nullable=False), sa.Column('student_id', sa.Integer(), nullable=False), - sa.Column('coach_id', sa.Integer(), nullable=False), - sa.Column('suggestion', sa.Enum('UNDECIDED', 'YES', 'MAYBE', 'NO', name='decisionenum'), nullable=False), + sa.Column('coach_id', sa.Integer(), nullable=True), + sa.Column('suggestion', sa.Enum('UNDECIDED', 'YES', 'MAYBE', 'NO', name='decisionenum'), + nullable=False), sa.Column('argumentation', sa.Text(), nullable=True), sa.ForeignKeyConstraint(['coach_id'], ['users.user_id'], name='suggestions_users_fk'), sa.ForeignKeyConstraint(['student_id'], ['students.student_id'], name='suggestions_students_fk'), - sa.PrimaryKeyConstraint('suggestion_id') + sa.PrimaryKeyConstraint('suggestion_id'), + sa.UniqueConstraint('student_id', 'coach_id') + ) + op.create_table( + 'pr_suggestions', + sa.Column('project_role_suggestion_id', sa.Integer(), nullable=False), + sa.Column('argumentation', sa.Text(), nullable=True), + sa.Column('project_role_id', sa.Integer(), nullable=True), + sa.Column('student_id', sa.Integer(), nullable=True), + sa.Column('drafter_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['drafter_id'], ['users.user_id'], name='pr_suggestions_users_fk'), + sa.ForeignKeyConstraint( + ['project_role_id'], + ['project_roles.project_role_id'], + name='pr_suggestions_project_roles_fk' + ), + sa.ForeignKeyConstraint(['student_id'], ['students.student_id'], name='pr_suggestions_students_fk'), + sa.PrimaryKeyConstraint('project_role_suggestion_id'), + sa.UniqueConstraint('project_role_id', 'student_id') + ) + op.create_table( + 'question_answers', + sa.Column('answer_id', sa.Integer(), nullable=False), + sa.Column('answer', sa.Text(), nullable=True), + sa.Column('question_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['question_id'], ['questions.question_id'], name='question_answers_questions_fk'), + sa.PrimaryKeyConstraint('answer_id') + ) + op.create_table( + 'question_file_answers', + sa.Column('file_answer_id', sa.Integer(), nullable=False), + sa.Column('file_name', sa.Text(), nullable=False), + sa.Column('url', sa.Text(), nullable=False), + sa.Column('mime_type', sa.Text(), nullable=False), + sa.Column('size', sa.Integer(), nullable=False), + sa.Column('question_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['question_id'], ['questions.question_id'], name='question_file_answers_questions_fk'), + sa.PrimaryKeyConstraint('file_answer_id') ) - # ### end Alembic commands ### def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('question_file_answers') + op.drop_table('question_answers') + op.drop_table('pr_suggestions') op.drop_table('suggestions') op.drop_table('student_skills') + op.drop_table('questions') op.drop_table('project_roles') - op.drop_table('decision_emails') - op.drop_table('students') - op.drop_table('project_skills') op.drop_table('project_partners') op.drop_table('project_coaches') - op.drop_table('webhooks') - op.drop_table('user_roles') + op.drop_table('decision_emails') + op.drop_table('webhook_urls') + op.drop_table('user_editions') + op.drop_table('students') op.drop_table('projects') + op.drop_table('invite_links') op.drop_table('google_auths') op.drop_table('github_auths') op.drop_table('email_auths') @@ -208,6 +288,4 @@ def downgrade(): op.drop_table('users') op.drop_table('skills') op.drop_table('partners') - op.drop_table('invite_links') op.drop_table('editions') - # ### end Alembic commands ### diff --git a/backend/migrations/versions/64c42bb48aee_.py b/backend/migrations/versions/64c42bb48aee_.py deleted file mode 100644 index 23597345c..000000000 --- a/backend/migrations/versions/64c42bb48aee_.py +++ /dev/null @@ -1,24 +0,0 @@ -"""empty message - -Revision ID: 64c42bb48aee -Revises: 964637070800, d4eaf2b564a4 -Create Date: 2022-04-22 16:25:02.453857 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '64c42bb48aee' -down_revision = ('964637070800', 'd4eaf2b564a4') -branch_labels = None -depends_on = None - - -def upgrade(): - pass - - -def downgrade(): - pass diff --git a/backend/migrations/versions/73e669bda33d_add_admin_field_remove_userrole_table_.py b/backend/migrations/versions/73e669bda33d_add_admin_field_remove_userrole_table_.py deleted file mode 100644 index 743cb41c6..000000000 --- a/backend/migrations/versions/73e669bda33d_add_admin_field_remove_userrole_table_.py +++ /dev/null @@ -1,55 +0,0 @@ -"""add admin field, remove userrole table, link user to edition - -Revision ID: 73e669bda33d -Revises: 145816a4b2df -Create Date: 2022-03-12 22:01:41.966008 - -""" -import sqlalchemy as sa -from alembic import op -from sqlalchemy import Enum -from sqlalchemy.dialects import mysql - -# revision identifiers, used by Alembic. -revision = '73e669bda33d' -down_revision = '145816a4b2df' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - 'user_editions', - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('edition_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['edition_id'], ['editions.edition_id'], 'user_editions_editions_fk'), - sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], 'user_editions_users_fk') - ) - op.drop_table('user_roles') - with op.batch_alter_table('users', schema=None) as batch_op: - batch_op.add_column(sa.Column('admin', sa.Boolean(), nullable=False, server_default=sa.sql.expression.literal(False))) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('users', schema=None) as batch_op: - batch_op.drop_column('admin') - - op.create_table( - 'user_roles', - sa.Column('user_role_id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False), - sa.Column('user_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True), - sa.Column('role', Enum('ADMIN', 'COACH'), nullable=True), - sa.Column('edition_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['edition_id'], ['editions.edition_id'], name='user_roles_editions_fk'), - sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], name='user_roles_users_fk'), - sa.PrimaryKeyConstraint('user_role_id'), - mariadb_default_charset='latin1', - mariadb_engine='InnoDB' - ) - - op.drop_table('user_editions') - # ### end Alembic commands ### diff --git a/backend/migrations/versions/810c6967f5d5_make_roles_a_list_make_edition_year_.py b/backend/migrations/versions/810c6967f5d5_make_roles_a_list_make_edition_year_.py deleted file mode 100644 index a0f4091cc..000000000 --- a/backend/migrations/versions/810c6967f5d5_make_roles_a_list_make_edition_year_.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Make roles a list, make edition year unique - -Revision ID: 810c6967f5d5 -Revises: d29dfe421181 -Create Date: 2022-03-06 15:07:34.230577 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '810c6967f5d5' -down_revision = 'd29dfe421181' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('editions', schema=None) as batch_op: - batch_op.create_unique_constraint("uq_editions_year", ['year']) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('editions', schema=None) as batch_op: - batch_op.drop_constraint("uq_editions_year", type_='unique') - - # ### end Alembic commands ### diff --git a/backend/migrations/versions/8c97ecc58e5f_multiple_column_unique_constraint_coach_.py b/backend/migrations/versions/8c97ecc58e5f_multiple_column_unique_constraint_coach_.py deleted file mode 100644 index 91354c241..000000000 --- a/backend/migrations/versions/8c97ecc58e5f_multiple_column_unique_constraint_coach_.py +++ /dev/null @@ -1,32 +0,0 @@ -"""multiple column unique constraint coach & student suggestion - -Revision ID: 8c97ecc58e5f -Revises: f125e90b2cf3 -Create Date: 2022-03-16 21:07:44.193388 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '8c97ecc58e5f' -down_revision = 'f125e90b2cf3' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('suggestions', schema=None) as batch_op: - batch_op.create_unique_constraint('unique_coach_student_suggestion', ['coach_id', 'student_id']) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('suggestions', schema=None) as batch_op: - batch_op.drop_constraint('unique_coach_student_suggestion', type_='unique') - - # ### end Alembic commands ### diff --git a/backend/migrations/versions/964637070800_.py b/backend/migrations/versions/964637070800_.py deleted file mode 100644 index 9c4080542..000000000 --- a/backend/migrations/versions/964637070800_.py +++ /dev/null @@ -1,24 +0,0 @@ -"""empty message - -Revision ID: 964637070800 -Revises: a4a047b881db, c5bdaa5815ca -Create Date: 2022-04-08 20:25:41.099295 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '964637070800' -down_revision = ('a4a047b881db', 'c5bdaa5815ca') -branch_labels = None -depends_on = None - - -def upgrade(): - pass - - -def downgrade(): - pass diff --git a/backend/migrations/versions/a4a047b881db_add_email_to_google_github_auth.py b/backend/migrations/versions/a4a047b881db_add_email_to_google_github_auth.py deleted file mode 100644 index f293ca85c..000000000 --- a/backend/migrations/versions/a4a047b881db_add_email_to_google_github_auth.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Add email to Google & GitHub auth - -Revision ID: a4a047b881db -Revises: a5f19eb19cca -Create Date: 2022-04-06 17:40:15.305860 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'a4a047b881db' -down_revision = 'a5f19eb19cca' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('github_auths', schema=None) as batch_op: - batch_op.add_column(sa.Column('email', sa.Text(), nullable=False)) - batch_op.create_unique_constraint("uq_github_auths_email", ['email']) - - with op.batch_alter_table('google_auths', schema=None) as batch_op: - batch_op.add_column(sa.Column('email', sa.Text(), nullable=False)) - batch_op.create_unique_constraint("uq_google_auths_email", ['email']) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('google_auths', schema=None) as batch_op: - batch_op.drop_constraint("uq_google_auths_email", type_='unique') - batch_op.drop_column('email') - - with op.batch_alter_table('github_auths', schema=None) as batch_op: - batch_op.drop_constraint("uq_github_auths_email", type_='unique') - batch_op.drop_column('email') - - # ### end Alembic commands ### diff --git a/backend/migrations/versions/a5f19eb19cca_add_name_to_edition.py b/backend/migrations/versions/a5f19eb19cca_add_name_to_edition.py deleted file mode 100644 index 3fe73026f..000000000 --- a/backend/migrations/versions/a5f19eb19cca_add_name_to_edition.py +++ /dev/null @@ -1,34 +0,0 @@ -"""add name to edition - -Revision ID: a5f19eb19cca -Revises: ca4c4182b93a -Create Date: 2022-03-27 14:16:49.714952 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'a5f19eb19cca' -down_revision = 'ca4c4182b93a' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('editions', schema=None) as batch_op: - batch_op.add_column(sa.Column('name', sa.Text(), nullable=False)) - batch_op.create_unique_constraint("uq_edition_name", ['name']) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('editions', schema=None) as batch_op: - batch_op.drop_constraint("uq_edition_name", type_='unique') - batch_op.drop_column('name') - - # ### end Alembic commands ### diff --git a/backend/migrations/versions/b6a373574e13_link_invitation_links_to_editions.py b/backend/migrations/versions/b6a373574e13_link_invitation_links_to_editions.py deleted file mode 100644 index b9c3047cd..000000000 --- a/backend/migrations/versions/b6a373574e13_link_invitation_links_to_editions.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Link invitation links to editions - -Revision ID: b6a373574e13 -Revises: 6433759fff33 -Create Date: 2022-03-05 19:01:52.244338 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'b6a373574e13' -down_revision = '6433759fff33' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('invite_links', schema=None) as batch_op: - batch_op.add_column(sa.Column('edition_id', sa.Integer(), nullable=True)) - batch_op.create_foreign_key('invite_links_editions_fk', 'editions', ['edition_id'], ['edition_id']) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('invite_links', schema=None) as batch_op: - batch_op.drop_constraint('invite_links_editions_fk', type_='foreignkey') - batch_op.drop_column('edition_id') - - # ### end Alembic commands ### diff --git a/backend/migrations/versions/c5bdaa5815ca_.py b/backend/migrations/versions/c5bdaa5815ca_.py deleted file mode 100644 index 6de85ba03..000000000 --- a/backend/migrations/versions/c5bdaa5815ca_.py +++ /dev/null @@ -1,24 +0,0 @@ -"""empty message - -Revision ID: c5bdaa5815ca -Revises: a5f19eb19cca -Create Date: 2022-04-06 10:46:56.160993 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'c5bdaa5815ca' -down_revision = 'a5f19eb19cca' -branch_labels = None -depends_on = None - - -def upgrade(): - pass - - -def downgrade(): - pass diff --git a/backend/migrations/versions/ca4c4182b93a_moved_email_to_authemail.py b/backend/migrations/versions/ca4c4182b93a_moved_email_to_authemail.py deleted file mode 100644 index 9973803f7..000000000 --- a/backend/migrations/versions/ca4c4182b93a_moved_email_to_authemail.py +++ /dev/null @@ -1,40 +0,0 @@ -"""moved email to AuthEmail - -Revision ID: ca4c4182b93a -Revises: f125e90b2cf3 -Create Date: 2022-03-27 10:47:50.051982 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql - -# revision identifiers, used by Alembic. -revision = 'ca4c4182b93a' -down_revision = 'f125e90b2cf3' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('email_auths', schema=None) as batch_op: - batch_op.add_column(sa.Column('email', sa.Text(), nullable=False)) - batch_op.create_unique_constraint("uq_email_auth_email", ['email']) - - with op.batch_alter_table('users', schema=None) as batch_op: - batch_op.drop_column('email') - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('users', schema=None) as batch_op: - batch_op.add_column(sa.Column('email', mysql.TEXT(), nullable=True)) - - with op.batch_alter_table('email_auths', schema=None) as batch_op: - batch_op.drop_constraint("uq_email_auth_email", type_='unique') - batch_op.drop_column('email') - - # ### end Alembic commands ### diff --git a/backend/migrations/versions/d29dfe421181_remove_leftover_refactored_association_.py b/backend/migrations/versions/d29dfe421181_remove_leftover_refactored_association_.py deleted file mode 100644 index 9db0ffbc4..000000000 --- a/backend/migrations/versions/d29dfe421181_remove_leftover_refactored_association_.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Remove leftover refactored association tables - -Revision ID: d29dfe421181 -Revises: b6a373574e13 -Create Date: 2022-03-05 22:43:56.712372 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'd29dfe421181' -down_revision = 'b6a373574e13' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### diff --git a/backend/migrations/versions/d4eaf2b564a4_merge_heads.py b/backend/migrations/versions/d4eaf2b564a4_merge_heads.py deleted file mode 100644 index 410472ae3..000000000 --- a/backend/migrations/versions/d4eaf2b564a4_merge_heads.py +++ /dev/null @@ -1,24 +0,0 @@ -"""merge heads - -Revision ID: d4eaf2b564a4 -Revises: 43e6e98fe039, 1862d7dea4cc -Create Date: 2022-04-19 09:53:31.222511 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'd4eaf2b564a4' -down_revision = ('43e6e98fe039', '1862d7dea4cc') -branch_labels = None -depends_on = None - - -def upgrade(): - pass - - -def downgrade(): - pass diff --git a/backend/migrations/versions/f125e90b2cf3_link_editions_to_coach_requests.py b/backend/migrations/versions/f125e90b2cf3_link_editions_to_coach_requests.py deleted file mode 100644 index 9f63b3b00..000000000 --- a/backend/migrations/versions/f125e90b2cf3_link_editions_to_coach_requests.py +++ /dev/null @@ -1,34 +0,0 @@ -"""link editions to coach requests - -Revision ID: f125e90b2cf3 -Revises: 73e669bda33d -Create Date: 2022-03-13 22:16:01.606059 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'f125e90b2cf3' -down_revision = '73e669bda33d' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('coach_requests', schema=None) as batch_op: - batch_op.add_column(sa.Column('edition_id', sa.Integer(), nullable=False)) - batch_op.create_foreign_key("coach_requests_editions_fk", 'editions', ['edition_id'], ['edition_id']) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('coach_requests', schema=None) as batch_op: - batch_op.drop_constraint("coach_requests_editions_fk", type_='foreignkey') - batch_op.drop_column('edition_id') - - # ### end Alembic commands ### diff --git a/backend/poetry.lock b/backend/poetry.lock index 7ecdcb9c2..b378a786e 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1,6 +1,48 @@ +[[package]] +name = "aiohttp" +version = "3.8.1" +description = "Async http client/server framework (asyncio)" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = ">=4.0.0a3,<5.0" +attrs = ">=17.3.0" +charset-normalizer = ">=2.0,<3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["aiodns", "brotli", "cchardet"] + +[[package]] +name = "aiosignal" +version = "1.2.0" +description = "aiosignal: a list of registered asynchronous callbacks" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "aiosqlite" +version = "0.17.0" +description = "asyncio bridge to the standard sqlite3 module" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing_extensions = ">=3.7.2" + [[package]] name = "alembic" -version = "1.7.6" +version = "1.7.7" description = "A database migration tool for SQLAlchemy." category = "main" optional = false @@ -15,7 +57,7 @@ tz = ["python-dateutil"] [[package]] name = "anyio" -version = "3.5.0" +version = "3.6.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" category = "main" optional = false @@ -27,12 +69,12 @@ sniffio = ">=1.1" [package.extras] doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] trio = ["trio (>=0.16)"] [[package]] name = "asgiref" -version = "3.5.0" +version = "3.5.2" description = "ASGI specs, helper code, and adapters" category = "main" optional = false @@ -43,7 +85,7 @@ tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] [[package]] name = "astroid" -version = "2.9.3" +version = "2.11.5" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -51,7 +93,23 @@ python-versions = ">=3.6.2" [package.dependencies] lazy-object-proxy = ">=1.4.0" -wrapt = ">=1.11,<1.14" +wrapt = ">=1.11,<2" + +[[package]] +name = "async-timeout" +version = "4.0.2" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "asyncmy" +version = "0.2.5" +description = "A fast asyncio MySQL driver" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" [[package]] name = "atomicwrites" @@ -65,7 +123,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "attrs" version = "21.4.0" description = "Classes Without Boilerplate" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -77,7 +135,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (> [[package]] name = "bcrypt" -version = "3.2.0" +version = "3.2.2" description = "Modern password hashing for your software and your servers" category = "main" optional = false @@ -85,7 +143,6 @@ python-versions = ">=3.6" [package.dependencies] cffi = ">=1.1" -six = ">=1.4.1" [package.extras] tests = ["pytest (>=3.2.1,!=3.3.0)"] @@ -123,11 +180,11 @@ unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.4" +version = "8.1.3" description = "Composable command line interface toolkit" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -142,7 +199,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.3.1" +version = "6.3.3" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -156,7 +213,7 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "36.0.2" +version = "37.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -171,7 +228,18 @@ docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + +[[package]] +name = "dill" +version = "0.3.4" +description = "serialize all of python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*" + +[package.extras] +graph = ["objgraph (>=1.7.2)"] [[package]] name = "ecdsa" @@ -208,7 +276,7 @@ tests = ["pytest", "dj-database-url", "dj-email-url", "django-cache-url"] [[package]] name = "faker" -version = "13.3.1" +version = "13.11.1" description = "Faker is a Python package that generates fake data for you." category = "dev" optional = false @@ -219,7 +287,7 @@ python-dateutil = ">=2.4" [[package]] name = "fastapi" -version = "0.74.1" +version = "0.78.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false @@ -227,13 +295,21 @@ python-versions = ">=3.6.1" [package.dependencies] pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = "0.17.1" +starlette = "0.19.1" [package.extras] -all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] -dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] -doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"] -test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==21.9b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"] +all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] +dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer (>=0.4.1,<0.5.0)", "pyyaml (>=5.3.1,<7.0.0)"] +test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==22.3.0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==4.2.1)", "types-orjson (==3.6.2)", "types-dataclasses (==0.6.5)"] + +[[package]] +name = "frozenlist" +version = "1.3.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +category = "main" +optional = false +python-versions = ">=3.7" [[package]] name = "greenlet" @@ -248,22 +324,61 @@ docs = ["sphinx"] [[package]] name = "h11" -version = "0.13.0" +version = "0.12.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "httpcore" +version = "0.14.7" +description = "A minimal low-level HTTP client." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +anyio = ">=3.0.0,<4.0.0" +certifi = "*" +h11 = ">=0.11,<0.13" +sniffio = ">=1.0.0,<2.0.0" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + [[package]] name = "httptools" -version = "0.2.0" +version = "0.4.0" description = "A collection of framework independent HTTP protocol utils." category = "main" optional = false -python-versions = "*" +python-versions = ">=3.5.0" [package.extras] -test = ["Cython (==0.29.22)"] +test = ["Cython (>=0.29.24,<0.30.0)"] + +[[package]] +name = "httpx" +version = "0.22.0" +description = "The next generation HTTP client." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +certifi = "*" +charset-normalizer = "*" +httpcore = ">=0.14.5,<0.15.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +sniffio = "*" + +[package.extras] +brotli = ["brotlicffi", "brotli"] +cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10.0.0,<11.0.0)", "pygments (>=2.0.0,<3.0.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "idna" @@ -321,7 +436,7 @@ testing = ["pytest"] [[package]] name = "mariadb" -version = "1.0.10" +version = "1.0.11" description = "Python MariaDB extension" category = "main" optional = false @@ -360,9 +475,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "multidict" +version = "6.0.2" +description = "multidict implementation" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "mypy" -version = "0.940" +version = "0.950" description = "Optional static typing for Python" category = "dev" optional = false @@ -370,7 +493,7 @@ python-versions = ">=3.6" [package.dependencies] mypy-extensions = ">=0.4.3" -tomli = ">=1.1.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = ">=3.10" [package.extras] @@ -416,15 +539,15 @@ totp = ["cryptography"] [[package]] name = "platformdirs" -version = "2.5.1" +version = "2.5.2" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] [[package]] name = "pluggy" @@ -479,7 +602,7 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pyhumps" -version = "3.5.3" +version = "3.7.1" description = "🐫 Convert strings (and dictionary keys) between snake case, camel case and pascal case in Python. Inspired by Humps for Node" category = "main" optional = false @@ -487,19 +610,23 @@ python-versions = "*" [[package]] name = "pylint" -version = "2.12.2" +version = "2.13.9" description = "python code static checker" category = "dev" optional = false python-versions = ">=3.6.2" [package.dependencies] -astroid = ">=2.9.0,<2.10" +astroid = ">=2.11.5,<=2.12.0-dev0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +dill = ">=0.2" isort = ">=4.2.5,<6" -mccabe = ">=0.6,<0.7" +mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" -toml = ">=0.9.2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +testutil = ["gitpython (>3)"] [[package]] name = "pylint-pytest" @@ -515,22 +642,22 @@ pytest = ">=4.6" [[package]] name = "pyparsing" -version = "3.0.7" -description = "Python parsing module" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6.8" [package.extras] -diagrams = ["jinja2", "railroad-diagrams"] +diagrams = ["railroad-diagrams", "jinja2"] [[package]] name = "pytest" -version = "7.0.1" +version = "7.1.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} @@ -545,6 +672,20 @@ tomli = ">=1.0.0" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.18.3" +description = "Pytest support for asyncio" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=6.1.0" + +[package.extras] +testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"] + [[package]] name = "pytest-cov" version = "3.0.0" @@ -598,7 +739,7 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "0.19.2" +version = "0.20.0" description = "Read key-value pairs from a .env file and set them as environment variables" category = "main" optional = false @@ -663,6 +804,20 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + [[package]] name = "rsa" version = "4.8" @@ -692,7 +847,7 @@ python-versions = ">=3.5" [[package]] name = "sqlalchemy" -version = "1.4.31" +version = "1.4.36" description = "Database Abstraction Library" category = "main" optional = false @@ -705,7 +860,7 @@ greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platfo aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"] asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3)"] +asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3,!=0.2.4)"] mariadb_connector = ["mariadb (>=1.0.1)"] mssql = ["pyodbc"] mssql_pymssql = ["pymssql"] @@ -750,7 +905,7 @@ url = ["furl (>=0.4.1)"] [[package]] name = "sqlalchemy2-stubs" -version = "0.0.2a20" +version = "0.0.2a22" description = "Typing Stubs for SQLAlchemy 1.4" category = "dev" optional = false @@ -761,26 +916,18 @@ typing-extensions = ">=3.7.4" [[package]] name = "starlette" -version = "0.17.1" +version = "0.19.1" description = "The little ASGI library that shines." category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -anyio = ">=3.0.0,<4" +anyio = ">=3.4.0,<5" [package.extras] full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - [[package]] name = "tomli" version = "2.0.1" @@ -791,7 +938,7 @@ python-versions = ">=3.7" [[package]] name = "types-passlib" -version = "1.7.0" +version = "1.7.5" description = "Typing stubs for passlib" category = "dev" optional = false @@ -799,11 +946,11 @@ python-versions = "*" [[package]] name = "typing-extensions" -version = "4.1.1" -description = "Backported and Experimental Type Hints for Python 3.6+" +version = "4.2.0" +description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "urllib3" @@ -820,26 +967,26 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uvicorn" -version = "0.15.0" +version = "0.17.6" description = "The lightning-fast ASGI server." category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7" [package.dependencies] asgiref = ">=3.4.0" click = ">=7.0" colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} h11 = ">=0.8" -httptools = {version = ">=0.2.0,<0.3.0", optional = true, markers = "extra == \"standard\""} +httptools = {version = ">=0.4.0", optional = true, markers = "extra == \"standard\""} python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} PyYAML = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} watchgod = {version = ">=0.6", optional = true, markers = "extra == \"standard\""} -websockets = {version = ">=9.1", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.0", optional = true, markers = "extra == \"standard\""} [package.extras] -standard = ["websockets (>=9.1)", "httptools (>=0.2.0,<0.3.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] +standard = ["websockets (>=10.0)", "httptools (>=0.4.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] [[package]] name = "uvloop" @@ -856,7 +1003,7 @@ test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,< [[package]] name = "watchgod" -version = "0.8" +version = "0.8.2" description = "Simple, modern file watching and code reload in python." category = "main" optional = false @@ -867,7 +1014,7 @@ anyio = ">=3.0.0,<4" [[package]] name = "websockets" -version = "10.2" +version = "10.3" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" category = "main" optional = false @@ -881,27 +1028,140 @@ category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +[[package]] +name = "yarl" +version = "1.7.2" +description = "Yet another URL library" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "5bf64e89fc7b51ce108190ab5525229fc27a35b889808d5507c3d2f22f5de71d" +content-hash = "79d682613b18663460ded7f8a531fd0045535b50da70db57bbbea4ac29505c22" [metadata.files] +aiohttp = [ + {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"}, + {file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"}, + {file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"}, + {file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"}, + {file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"}, + {file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"}, + {file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"}, + {file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"}, + {file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"}, + {file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"}, + {file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"}, + {file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"}, + {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"}, + {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"}, +] +aiosignal = [ + {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, + {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, +] +aiosqlite = [ + {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"}, + {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, +] alembic = [ - {file = "alembic-1.7.6-py3-none-any.whl", hash = "sha256:ad842f2c3ab5c5d4861232730779c05e33db4ba880a08b85eb505e87c01095bc"}, - {file = "alembic-1.7.6.tar.gz", hash = "sha256:6c0c05e9768a896d804387e20b299880fe01bc56484246b0dffe8075d6d3d847"}, + {file = "alembic-1.7.7-py3-none-any.whl", hash = "sha256:29be0856ec7591c39f4e1cb10f198045d890e6e2274cf8da80cb5e721a09642b"}, + {file = "alembic-1.7.7.tar.gz", hash = "sha256:4961248173ead7ce8a21efb3de378f13b8398e6630fab0eb258dc74a8af24c58"}, ] anyio = [ - {file = "anyio-3.5.0-py3-none-any.whl", hash = "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e"}, - {file = "anyio-3.5.0.tar.gz", hash = "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6"}, + {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, + {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, ] asgiref = [ - {file = "asgiref-3.5.0-py3-none-any.whl", hash = "sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9"}, - {file = "asgiref-3.5.0.tar.gz", hash = "sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0"}, + {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, + {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, ] astroid = [ - {file = "astroid-2.9.3-py3-none-any.whl", hash = "sha256:506daabe5edffb7e696ad82483ad0228245a9742ed7d2d8c9cdb31537decf9f6"}, - {file = "astroid-2.9.3.tar.gz", hash = "sha256:1efdf4e867d4d8ba4a9f6cf9ce07cd182c4c41de77f23814feb27ca93ca9d877"}, + {file = "astroid-2.11.5-py3-none-any.whl", hash = "sha256:14ffbb4f6aa2cf474a0834014005487f7ecd8924996083ab411e7fa0b508ce0b"}, + {file = "astroid-2.11.5.tar.gz", hash = "sha256:f4e4ec5294c4b07ac38bab9ca5ddd3914d4bf46f9006eb5c0ae755755061044e"}, +] +async-timeout = [ + {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, + {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, +] +asyncmy = [ + {file = "asyncmy-0.2.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:f176b55c82d3bdb10f0ecb3a518a54777f3d247e00f06add138f63df4edc0e3d"}, + {file = "asyncmy-0.2.5-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:f404d351d4f9fc741cdb8b49da8278e63a8551be6ccd03b514c2c0828500633d"}, + {file = "asyncmy-0.2.5-cp310-cp310-win_amd64.whl", hash = "sha256:9824184d180753d23101b1d9fdc7955783b08fbc6f3cc88d6acce7ff019c0eaf"}, + {file = "asyncmy-0.2.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:02add8063bd00762a469dcb9915563f66520ffda598851a957443b1012182f43"}, + {file = "asyncmy-0.2.5-cp37-cp37m-manylinux_2_31_x86_64.whl", hash = "sha256:a341d8a22d9a1f3236a81f17836002291567aa7d878ab75b37ed9bf4ce401290"}, + {file = "asyncmy-0.2.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2dc25f0bb723a71445d5c5b46ab2ae71c9dd3123fea9eae1bee4c36af5eda2bc"}, + {file = "asyncmy-0.2.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:e184e701fa153dd456f6b351136c0a7a7524b8f5959f7f4b47848efe434a9c7d"}, + {file = "asyncmy-0.2.5-cp38-cp38-manylinux_2_31_x86_64.whl", hash = "sha256:2275cc0036fa39f888c0770173ffb8ed16b89eecf2e380499275bfa6bf4ee088"}, + {file = "asyncmy-0.2.5-cp38-cp38-win_amd64.whl", hash = "sha256:527ccc24a9bd76d565f1024f4a50ee6fd92cac518e6d884281ad61ffc424e7cd"}, + {file = "asyncmy-0.2.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:6606e296dc161d71629f1163c3c82dda97431e25ee7240b6b7f139c2c35cff94"}, + {file = "asyncmy-0.2.5-cp39-cp39-manylinux_2_31_x86_64.whl", hash = "sha256:61ed1b9e69dafe476f02c20962b8ee4ec5ba130b760bdf504015fcb80bbe6787"}, + {file = "asyncmy-0.2.5-cp39-cp39-win_amd64.whl", hash = "sha256:f5dcc1a29d608cc5270f37328253032c554321b85bcf5653f1affa01f185c29c"}, + {file = "asyncmy-0.2.5.tar.gz", hash = "sha256:cf3ef3d1ada385d231f23fffcbf1fe040bae8575b187746633fbef6e7cff2844"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, @@ -912,16 +1172,17 @@ attrs = [ {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] bcrypt = [ - {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b589229207630484aefe5899122fb938a5b017b0f4349f769b8c13e78d99a8fd"}, - {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, - {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, - {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, - {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, - {file = "bcrypt-3.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a0584a92329210fcd75eb8a3250c5a941633f8bfaf2a18f81009b097732839b7"}, - {file = "bcrypt-3.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:56e5da069a76470679f312a7d3d23deb3ac4519991a0361abc11da837087b61d"}, - {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, - {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, - {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, + {file = "bcrypt-3.2.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:7180d98a96f00b1050e93f5b0f556e658605dd9f524d0b0e68ae7944673f525e"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:61bae49580dce88095d669226d5076d0b9d927754cedbdf76c6c9f5099ad6f26"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88273d806ab3a50d06bc6a2fc7c87d737dd669b76ad955f449c43095389bc8fb"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6d2cb9d969bfca5bc08e45864137276e4c3d3d7de2b162171def3d188bf9d34a"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b02d6bfc6336d1094276f3f588aa1225a598e27f8e3388f4db9948cb707b521"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2c46100e315c3a5b90fdc53e429c006c5f962529bc27e1dfd656292c20ccc40"}, + {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7d9ba2e41e330d2af4af6b1b6ec9e6128e91343d0b4afb9282e54e5508f31baa"}, + {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cd43303d6b8a165c29ec6756afd169faba9396a9472cdff753fe9f19b96ce2fa"}, + {file = "bcrypt-3.2.2-cp36-abi3-win32.whl", hash = "sha256:4e029cef560967fb0cf4a802bcf4d562d3d6b4b1bf81de5ec1abbe0f1adb027e"}, + {file = "bcrypt-3.2.2-cp36-abi3-win_amd64.whl", hash = "sha256:7ff2069240c6bbe49109fe84ca80508773a904f5a8cb960e02a977f7f519b129"}, + {file = "bcrypt-3.2.2.tar.gz", hash = "sha256:433c410c2177057705da2a9f2cd01dd157493b2a7ac14c8593a16b3dab6b6bfb"}, ] certifi = [ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, @@ -984,77 +1245,83 @@ charset-normalizer = [ {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] click = [ - {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, - {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-6.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525"}, - {file = "coverage-6.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c"}, - {file = "coverage-6.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145"}, - {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce"}, - {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167"}, - {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda"}, - {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27"}, - {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e"}, - {file = "coverage-6.3.1-cp310-cp310-win32.whl", hash = "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217"}, - {file = "coverage-6.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb"}, - {file = "coverage-6.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0"}, - {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793"}, - {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd"}, - {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1"}, - {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554"}, - {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"}, - {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8"}, - {file = "coverage-6.3.1-cp37-cp37m-win32.whl", hash = "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0"}, - {file = "coverage-6.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687"}, - {file = "coverage-6.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320"}, - {file = "coverage-6.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8"}, - {file = "coverage-6.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734"}, - {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4"}, - {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975"}, - {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa"}, - {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b"}, - {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a"}, - {file = "coverage-6.3.1-cp38-cp38-win32.whl", hash = "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10"}, - {file = "coverage-6.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f"}, - {file = "coverage-6.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d"}, - {file = "coverage-6.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6"}, - {file = "coverage-6.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1"}, - {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c"}, - {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba"}, - {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed"}, - {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f"}, - {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38"}, - {file = "coverage-6.3.1-cp39-cp39-win32.whl", hash = "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2"}, - {file = "coverage-6.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa"}, - {file = "coverage-6.3.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2"}, - {file = "coverage-6.3.1.tar.gz", hash = "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8"}, + {file = "coverage-6.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df32ee0f4935a101e4b9a5f07b617d884a531ed5666671ff6ac66d2e8e8246d8"}, + {file = "coverage-6.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75b5dbffc334e0beb4f6c503fb95e6d422770fd2d1b40a64898ea26d6c02742d"}, + {file = "coverage-6.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:114944e6061b68a801c5da5427b9173a0dd9d32cd5fcc18a13de90352843737d"}, + {file = "coverage-6.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab88a01cd180b5640ccc9c47232e31924d5f9967ab7edd7e5c91c68eee47a69"}, + {file = "coverage-6.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad8f9068f5972a46d50fe5f32c09d6ee11da69c560fcb1b4c3baea246ca4109b"}, + {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4cd696aa712e6cd16898d63cf66139dc70d998f8121ab558f0e1936396dbc579"}, + {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c1a9942e282cc9d3ed522cd3e3cab081149b27ea3bda72d6f61f84eaf88c1a63"}, + {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c06455121a089252b5943ea682187a4e0a5cf0a3fb980eb8e7ce394b144430a9"}, + {file = "coverage-6.3.3-cp310-cp310-win32.whl", hash = "sha256:cb5311d6ccbd22578c80028c5e292a7ab9adb91bd62c1982087fad75abe2e63d"}, + {file = "coverage-6.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:6d4a6f30f611e657495cc81a07ff7aa8cd949144e7667c5d3e680d73ba7a70e4"}, + {file = "coverage-6.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:79bf405432428e989cad7b8bc60581963238f7645ae8a404f5dce90236cc0293"}, + {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:338c417613f15596af9eb7a39353b60abec9d8ce1080aedba5ecee6a5d85f8d3"}, + {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db094a6a4ae6329ed322a8973f83630b12715654c197dd392410400a5bfa1a73"}, + {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1414e8b124611bf4df8d77215bd32cba6e3425da8ce9c1f1046149615e3a9a31"}, + {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:93b16b08f94c92cab88073ffd185070cdcb29f1b98df8b28e6649145b7f2c90d"}, + {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fbc86ae8cc129c801e7baaafe3addf3c8d49c9c1597c44bdf2d78139707c3c62"}, + {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b5ba058610e8289a07db2a57bce45a1793ec0d3d11db28c047aae2aa1a832572"}, + {file = "coverage-6.3.3-cp37-cp37m-win32.whl", hash = "sha256:8329635c0781927a2c6ae068461e19674c564e05b86736ab8eb29c420ee7dc20"}, + {file = "coverage-6.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:e5af1feee71099ae2e3b086ec04f57f9950e1be9ecf6c420696fea7977b84738"}, + {file = "coverage-6.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e814a4a5a1d95223b08cdb0f4f57029e8eab22ffdbae2f97107aeef28554517e"}, + {file = "coverage-6.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61f4fbf3633cb0713437291b8848634ea97f89c7e849c2be17a665611e433f53"}, + {file = "coverage-6.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3401b0d2ed9f726fadbfa35102e00d1b3547b73772a1de5508ef3bdbcb36afe7"}, + {file = "coverage-6.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8586b177b4407f988731eb7f41967415b2197f35e2a6ee1a9b9b561f6323c8e9"}, + {file = "coverage-6.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:892e7fe32191960da559a14536768a62e83e87bbb867e1b9c643e7e0fbce2579"}, + {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:afb03f981fadb5aed1ac6e3dd34f0488e1a0875623d557b6fad09b97a942b38a"}, + {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cbe91bc84be4e5ef0b1480d15c7b18e29c73bdfa33e07d3725da7d18e1b0aff2"}, + {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:91502bf27cbd5c83c95cfea291ef387469f2387508645602e1ca0fd8a4ba7548"}, + {file = "coverage-6.3.3-cp38-cp38-win32.whl", hash = "sha256:c488db059848702aff30aa1d90ef87928d4e72e4f00717343800546fdbff0a94"}, + {file = "coverage-6.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6534fcdfb5c503affb6b1130db7b5bfc8a0f77fa34880146f7a5c117987d0"}, + {file = "coverage-6.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc692c9ee18f0dd3214843779ba6b275ee4bb9b9a5745ba64265bce911aefd1a"}, + {file = "coverage-6.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:462105283de203df8de58a68c1bb4ba2a8a164097c2379f664fa81d6baf94b81"}, + {file = "coverage-6.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc972d829ad5ef4d4c5fcabd2bbe2add84ce8236f64ba1c0c72185da3a273130"}, + {file = "coverage-6.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06f54765cdbce99901871d50fe9f41d58213f18e98b170a30ca34f47de7dd5e8"}, + {file = "coverage-6.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7835f76a081787f0ca62a53504361b3869840a1620049b56d803a8cb3a9eeea3"}, + {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6f5fee77ec3384b934797f1873758f796dfb4f167e1296dc00f8b2e023ce6ee9"}, + {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:baa8be8aba3dd1e976e68677be68a960a633a6d44c325757aefaa4d66175050f"}, + {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4d06380e777dd6b35ee936f333d55b53dc4a8271036ff884c909cf6e94be8b6c"}, + {file = "coverage-6.3.3-cp39-cp39-win32.whl", hash = "sha256:f8cabc5fd0091976ab7b020f5708335033e422de25e20ddf9416bdce2b7e07d8"}, + {file = "coverage-6.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c9441d57b0963cf8340268ad62fc83de61f1613034b79c2b1053046af0c5284"}, + {file = "coverage-6.3.3-pp36.pp37.pp38-none-any.whl", hash = "sha256:d522f1dc49127eab0bfbba4e90fa068ecff0899bbf61bf4065c790ddd6c177fe"}, + {file = "coverage-6.3.3.tar.gz", hash = "sha256:2781c43bffbbec2b8867376d4d61916f5e9c4cc168232528562a61d1b4b01879"}, ] cryptography = [ - {file = "cryptography-36.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:4e2dddd38a5ba733be6a025a1475a9f45e4e41139d1321f412c6b360b19070b6"}, - {file = "cryptography-36.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:4881d09298cd0b669bb15b9cfe6166f16fc1277b4ed0d04a22f3d6430cb30f1d"}, - {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea634401ca02367c1567f012317502ef3437522e2fc44a3ea1844de028fa4b84"}, - {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7be666cc4599b415f320839e36367b273db8501127b38316f3b9f22f17a0b815"}, - {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8241cac0aae90b82d6b5c443b853723bcc66963970c67e56e71a2609dc4b5eaf"}, - {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2d54e787a884ffc6e187262823b6feb06c338084bbe80d45166a1cb1c6c5bf"}, - {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:c2c5250ff0d36fd58550252f54915776940e4e866f38f3a7866d92b32a654b86"}, - {file = "cryptography-36.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ec6597aa85ce03f3e507566b8bcdf9da2227ec86c4266bd5e6ab4d9e0cc8dab2"}, - {file = "cryptography-36.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ca9f686517ec2c4a4ce930207f75c00bf03d94e5063cbc00a1dc42531511b7eb"}, - {file = "cryptography-36.0.2-cp36-abi3-win32.whl", hash = "sha256:f64b232348ee82f13aac22856515ce0195837f6968aeaa94a3d0353ea2ec06a6"}, - {file = "cryptography-36.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:53e0285b49fd0ab6e604f4c5d9c5ddd98de77018542e88366923f152dbeb3c29"}, - {file = "cryptography-36.0.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:32db5cc49c73f39aac27574522cecd0a4bb7384e71198bc65a0d23f901e89bb7"}, - {file = "cryptography-36.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b3d199647468d410994dbeb8cec5816fb74feb9368aedf300af709ef507e3e"}, - {file = "cryptography-36.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:da73d095f8590ad437cd5e9faf6628a218aa7c387e1fdf67b888b47ba56a17f0"}, - {file = "cryptography-36.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:0a3bf09bb0b7a2c93ce7b98cb107e9170a90c51a0162a20af1c61c765b90e60b"}, - {file = "cryptography-36.0.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8897b7b7ec077c819187a123174b645eb680c13df68354ed99f9b40a50898f77"}, - {file = "cryptography-36.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82740818f2f240a5da8dfb8943b360e4f24022b093207160c77cadade47d7c85"}, - {file = "cryptography-36.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1f64a62b3b75e4005df19d3b5235abd43fa6358d5516cfc43d87aeba8d08dd51"}, - {file = "cryptography-36.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e167b6b710c7f7bc54e67ef593f8731e1f45aa35f8a8a7b72d6e42ec76afd4b3"}, - {file = "cryptography-36.0.2.tar.gz", hash = "sha256:70f8f4f7bb2ac9f340655cbac89d68c527af5bb4387522a8413e841e3e6628c9"}, + {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181"}, + {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336"}, + {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004"}, + {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe"}, + {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804"}, + {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c"}, + {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178"}, + {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a"}, + {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15"}, + {file = "cryptography-37.0.2-cp36-abi3-win32.whl", hash = "sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0"}, + {file = "cryptography-37.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d"}, + {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9"}, + {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1"}, + {file = "cryptography-37.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023"}, + {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06"}, + {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717"}, + {file = "cryptography-37.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f"}, + {file = "cryptography-37.0.2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982"}, + {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4"}, + {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de"}, + {file = "cryptography-37.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452"}, + {file = "cryptography-37.0.2.tar.gz", hash = "sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e"}, +] +dill = [ + {file = "dill-0.3.4-py2.py3-none-any.whl", hash = "sha256:7e40e4a70304fd9ceab3535d36e58791d9c4a776b38ec7f7ec9afc8d3dca4d4f"}, + {file = "dill-0.3.4.zip", hash = "sha256:9f9734205146b2b353ab3fec9af0070237b6ddae78452af83d2fca84d739e675"}, ] ecdsa = [ {file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"}, @@ -1065,12 +1332,73 @@ environs = [ {file = "environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"}, ] faker = [ - {file = "Faker-13.3.1-py3-none-any.whl", hash = "sha256:c88c8b5ee9376a242deca8fe829f9a3215ffa43c31da6f66d9594531fb344453"}, - {file = "Faker-13.3.1.tar.gz", hash = "sha256:fa060e331ffffb57cfa4c07f95d54911e339984ed72596ba6a9e7b6fa569d799"}, + {file = "Faker-13.11.1-py3-none-any.whl", hash = "sha256:c6ff91847d7c820afc0a74d95e824b48aab71ddfd9003f300641e42d58ae886f"}, + {file = "Faker-13.11.1.tar.gz", hash = "sha256:cad1f69d72a68878cd67855140b6fe3e44c11628971cd838595d289c98bc45de"}, ] fastapi = [ - {file = "fastapi-0.74.1-py3-none-any.whl", hash = "sha256:b8ec8400623ef0b2ff558ebe06753b349f8e3a5dd38afea650800f2644ddba34"}, - {file = "fastapi-0.74.1.tar.gz", hash = "sha256:b58a2c46df14f62ebe6f24a9439927539ba1959b9be55ba0e2f516a683e5b9d4"}, + {file = "fastapi-0.78.0-py3-none-any.whl", hash = "sha256:15fcabd5c78c266fa7ae7d8de9b384bfc2375ee0503463a6febbe3bab69d6f65"}, + {file = "fastapi-0.78.0.tar.gz", hash = "sha256:3233d4a789ba018578658e2af1a4bb5e38bdd122ff722b313666a9b2c6786a83"}, +] +frozenlist = [ + {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"}, + {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"}, + {file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"}, + {file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"}, + {file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"}, + {file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"}, + {file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"}, + {file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"}, + {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"}, + {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"}, + {file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"}, + {file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"}, + {file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"}, + {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"}, + {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"}, + {file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"}, + {file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"}, + {file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"}, + {file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"}, ] greenlet = [ {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, @@ -1130,25 +1458,52 @@ greenlet = [ {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, ] h11 = [ - {file = "h11-0.13.0-py3-none-any.whl", hash = "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"}, - {file = "h11-0.13.0.tar.gz", hash = "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06"}, + {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, + {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, +] +httpcore = [ + {file = "httpcore-0.14.7-py3-none-any.whl", hash = "sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade"}, + {file = "httpcore-0.14.7.tar.gz", hash = "sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1"}, ] httptools = [ - {file = "httptools-0.2.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:79dbc21f3612a78b28384e989b21872e2e3cf3968532601544696e4ed0007ce5"}, - {file = "httptools-0.2.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:78d03dd39b09c99ec917d50189e6743adbfd18c15d5944392d2eabda688bf149"}, - {file = "httptools-0.2.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:a23166e5ae2775709cf4f7ad4c2048755ebfb272767d244e1a96d55ac775cca7"}, - {file = "httptools-0.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3ab1f390d8867f74b3b5ee2a7ecc9b8d7f53750bd45714bf1cb72a953d7dfa77"}, - {file = "httptools-0.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a7594f9a010cdf1e16a58b3bf26c9da39bbf663e3b8d46d39176999d71816658"}, - {file = "httptools-0.2.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:01b392a166adcc8bc2f526a939a8aabf89fe079243e1543fd0e7dc1b58d737cb"}, - {file = "httptools-0.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:80ffa04fe8c8dfacf6e4cef8277347d35b0442c581f5814f3b0cf41b65c43c6e"}, - {file = "httptools-0.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d5682eeb10cca0606c4a8286a3391d4c3c5a36f0c448e71b8bd05be4e1694bfb"}, - {file = "httptools-0.2.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:a289c27ccae399a70eacf32df9a44059ca2ba4ac444604b00a19a6c1f0809943"}, - {file = "httptools-0.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:813871f961edea6cb2fe312f2d9b27d12a51ba92545380126f80d0de1917ea15"}, - {file = "httptools-0.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:cc9be041e428c10f8b6ab358c6b393648f9457094e1dcc11b4906026d43cd380"}, - {file = "httptools-0.2.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b08d00d889a118f68f37f3c43e359aab24ee29eb2e3fe96d64c6a2ba8b9d6557"}, - {file = "httptools-0.2.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fd3b8905e21431ad306eeaf56644a68fdd621bf8f3097eff54d0f6bdf7262065"}, - {file = "httptools-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:200fc1cdf733a9ff554c0bb97a4047785cfaad9875307d6087001db3eb2b417f"}, - {file = "httptools-0.2.0.tar.gz", hash = "sha256:94505026be56652d7a530ab03d89474dc6021019d6b8682281977163b3471ea0"}, + {file = "httptools-0.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcddfe70553be717d9745990dfdb194e22ee0f60eb8f48c0794e7bfeda30d2d5"}, + {file = "httptools-0.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1ee0b459257e222b878a6c09ccf233957d3a4dcb883b0847640af98d2d9aac23"}, + {file = "httptools-0.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceafd5e960b39c7e0d160a1936b68eb87c5e79b3979d66e774f0c77d4d8faaed"}, + {file = "httptools-0.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fdb9f9ed79bc6f46b021b3319184699ba1a22410a82204e6e89c774530069683"}, + {file = "httptools-0.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:abe829275cdd4174b4c4e65ad718715d449e308d59793bf3a931ee1bf7e7b86c"}, + {file = "httptools-0.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7af6bdbd21a2a25d6784f6d67f44f5df33ef39b6159543b9f9064d365c01f919"}, + {file = "httptools-0.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5d1fe6b6661022fd6cac541f54a4237496b246e6f1c0a6b41998ee08a1135afe"}, + {file = "httptools-0.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:48e48530d9b995a84d1d89ae6b3ec4e59ea7d494b150ac3bbc5e2ac4acce92cd"}, + {file = "httptools-0.4.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a113789e53ac1fa26edf99856a61e4c493868e125ae0dd6354cf518948fbbd5c"}, + {file = "httptools-0.4.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e2eb957787cbb614a0f006bfc5798ff1d90ac7c4dd24854c84edbdc8c02369e"}, + {file = "httptools-0.4.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:7ee9f226acab9085037582c059d66769862706e8e8cd2340470ceb8b3850873d"}, + {file = "httptools-0.4.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:701e66b59dd21a32a274771238025d58db7e2b6ecebbab64ceff51b8e31527ae"}, + {file = "httptools-0.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6a1a7dfc1f9c78a833e2c4904757a0f47ce25d08634dd2a52af394eefe5f9777"}, + {file = "httptools-0.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:903f739c9fb78dab8970b0f3ea51f21955b24b45afa77b22ff0e172fc11ef111"}, + {file = "httptools-0.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54bbd295f031b866b9799dd39cb45deee81aca036c9bff9f58ca06726f6494f1"}, + {file = "httptools-0.4.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3194f6d6443befa8d4db16c1946b2fc428a3ceb8ab32eb6f09a59f86104dc1a0"}, + {file = "httptools-0.4.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cd1295f52971097f757edfbfce827b6dbbfb0f7a74901ee7d4933dff5ad4c9af"}, + {file = "httptools-0.4.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:20a45bcf22452a10fa8d58b7dbdb474381f6946bf5b8933e3662d572bc61bae4"}, + {file = "httptools-0.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d1f27bb0f75bef722d6e22dc609612bfa2f994541621cd2163f8c943b6463dfe"}, + {file = "httptools-0.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7f7bfb74718f52d5ed47d608d507bf66d3bc01d4a8b3e6dd7134daaae129357b"}, + {file = "httptools-0.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a522d12e2ddbc2e91842ffb454a1aeb0d47607972c7d8fc88bd0838d97fb8a2a"}, + {file = "httptools-0.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2db44a0b294d317199e9f80123e72c6b005c55b625b57fae36de68670090fa48"}, + {file = "httptools-0.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c286985b5e194ca0ebb2908d71464b9be8f17cc66d6d3e330e8d5407248f56ad"}, + {file = "httptools-0.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3a4e165ca6204f34856b765d515d558dc84f1352033b8721e8d06c3e44930c3"}, + {file = "httptools-0.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:72aa3fbe636b16d22e04b5a9d24711b043495e0ecfe58080addf23a1a37f3409"}, + {file = "httptools-0.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9967d9758df505975913304c434cb9ab21e2c609ad859eb921f2f615a038c8de"}, + {file = "httptools-0.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f72b5d24d6730035128b238decdc4c0f2104b7056a7ca55cf047c106842ec890"}, + {file = "httptools-0.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:29bf97a5c532da9c7a04de2c7a9c31d1d54f3abd65a464119b680206bbbb1055"}, + {file = "httptools-0.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98993805f1e3cdb53de4eed02b55dcc953cdf017ba7bbb2fd89226c086a6d855"}, + {file = "httptools-0.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d9b90bf58f3ba04e60321a23a8723a1ff2a9377502535e70495e5ada8e6e6722"}, + {file = "httptools-0.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a99346ebcb801b213c591540837340bdf6fd060a8687518d01c607d338b7424"}, + {file = "httptools-0.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:645373c070080e632480a3d251d892cb795be3d3a15f86975d0f1aca56fd230d"}, + {file = "httptools-0.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:34d2903dd2a3dd85d33705b6fde40bf91fc44411661283763fd0746723963c83"}, + {file = "httptools-0.4.0.tar.gz", hash = "sha256:2c9a930c378b3d15d6b695fb95ebcff81a7395b4f9775c4f10a076beb0b2c1ff"}, +] +httpx = [ + {file = "httpx-0.22.0-py3-none-any.whl", hash = "sha256:e35e83d1d2b9b2a609ef367cc4c1e66fd80b750348b20cc9e19d1952fc2ca3f6"}, + {file = "httpx-0.22.0.tar.gz", hash = "sha256:d8e778f76d9bbd46af49e7f062467e3157a5a3d2ae4876a4bbfd8a51ed9c9cb4"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, @@ -1206,15 +1561,15 @@ mako = [ {file = "Mako-1.2.0.tar.gz", hash = "sha256:9a7c7e922b87db3686210cf49d5d767033a41d4010b284e747682c92bddd8b39"}, ] mariadb = [ - {file = "mariadb-1.0.10-cp310-cp310-win32.whl", hash = "sha256:a27ada21397f4939bffc93f5266cc5bb188aa7d54872ec1237b1295781460f25"}, - {file = "mariadb-1.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:9a9f5f72b32a11ea619243b32ccf34a99e3333466e666f061ad90e8b5e871ee3"}, - {file = "mariadb-1.0.10-cp37-cp37m-win32.whl", hash = "sha256:e2d5e3ec72e3195502deca357f75f842aee3648aaba9624c6676d3ecd34a370f"}, - {file = "mariadb-1.0.10-cp37-cp37m-win_amd64.whl", hash = "sha256:ec236f8ab200088ffd80a12d94cf4a589e1246a7982f3a6fcb9197a755c9abd2"}, - {file = "mariadb-1.0.10-cp38-cp38-win32.whl", hash = "sha256:bda35e5742e50894a225ab995d31984ef6e452723266d4e378d9addb9dc0c321"}, - {file = "mariadb-1.0.10-cp38-cp38-win_amd64.whl", hash = "sha256:6c04dc33894181ad127b8023f10455552d95732a700a8acad4751389f0b3a111"}, - {file = "mariadb-1.0.10-cp39-cp39-win32.whl", hash = "sha256:547e8a363bd5b211c98b084b92d72cc9b6f76da7d063ec0246f5bb85100690e3"}, - {file = "mariadb-1.0.10-cp39-cp39-win_amd64.whl", hash = "sha256:526938f6de1e3be87b87b9f9f46cdbcc502b35afb68dbd027108ad23a97c1d79"}, - {file = "mariadb-1.0.10.zip", hash = "sha256:79028ba6051173dad1ad0be7518389cab70239f92b4ff8b8813dae55c3f2c53d"}, + {file = "mariadb-1.0.11-cp310-cp310-win32.whl", hash = "sha256:4a583a80059d11f1895a2c93b1b7110948e87f0256da3e3222939a2530f0518e"}, + {file = "mariadb-1.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:63c5c7cf99335e5c961e1d65a323576c9cb834e1a6a8084a6a8b4ffd85ca6213"}, + {file = "mariadb-1.0.11-cp37-cp37m-win32.whl", hash = "sha256:db509f8142e1bf55973bbe5f46ba33d9832b4031d1e249d657cbe263119b3b17"}, + {file = "mariadb-1.0.11-cp37-cp37m-win_amd64.whl", hash = "sha256:61fd04f555de0a58d6519be6f1e5a70daaef2334ca8946a7c25c8a9a2d96c6e2"}, + {file = "mariadb-1.0.11-cp38-cp38-win32.whl", hash = "sha256:9afc253b9edbec95637465c01fe5c730dd549f18752cdc5cdfac995adfe7c8d3"}, + {file = "mariadb-1.0.11-cp38-cp38-win_amd64.whl", hash = "sha256:166973d6cd7da5d4fe84fc9d63f8d219a660ed2c82ebf6acadc9b3dd811f51bc"}, + {file = "mariadb-1.0.11-cp39-cp39-win32.whl", hash = "sha256:173f23f9a01b2043523168e73e110ade417d38f325abdb2271d4ebedd3abea25"}, + {file = "mariadb-1.0.11-cp39-cp39-win_amd64.whl", hash = "sha256:4c8a1e5ed4c242b7310876219e1efb364f03394db140513e6f7541de3c803d04"}, + {file = "mariadb-1.0.11.zip", hash = "sha256:76916c892bc936c5b0f36e25a1411f651a7b7ce978992ae3a8de7e283efdacbf"}, ] markupsafe = [ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, @@ -1266,30 +1621,91 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] +multidict = [ + {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"}, + {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"}, + {file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"}, + {file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"}, + {file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"}, + {file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"}, + {file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"}, + {file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"}, + {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"}, + {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"}, + {file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"}, + {file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"}, + {file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"}, + {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"}, + {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"}, + {file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"}, + {file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"}, + {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, + {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, +] mypy = [ - {file = "mypy-0.940-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0fdc9191a49c77ab5fa0439915d405e80a1118b163ab03cd2a530f346b12566a"}, - {file = "mypy-0.940-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1903c92ff8642d521b4627e51a67e49f5be5aedb1fb03465b3aae4c3338ec491"}, - {file = "mypy-0.940-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:471af97c35a32061883b0f8a3305ac17947fd42ce962ca9e2b0639eb9141492f"}, - {file = "mypy-0.940-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:13677cb8b050f03b5bb2e8bf7b2668cd918b001d56c2435082bbfc9d5f730f42"}, - {file = "mypy-0.940-cp310-cp310-win_amd64.whl", hash = "sha256:2efd76893fb8327eca7e942e21b373e6f3c5c083ff860fb1e82ddd0462d662bd"}, - {file = "mypy-0.940-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8fe1bfab792e4300f80013edaf9949b34e4c056a7b2531b5ef3a0fb9d598ae2"}, - {file = "mypy-0.940-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2dba92f58610d116f68ec1221fb2de2a346d081d17b24a784624389b17a4b3f9"}, - {file = "mypy-0.940-cp36-cp36m-win_amd64.whl", hash = "sha256:712affcc456de637e774448c73e21c84dfa5a70bcda34e9b0be4fb898a9e8e07"}, - {file = "mypy-0.940-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8aaf18d0f8bc3ffba56d32a85971dfbd371a5be5036da41ac16aefec440eff17"}, - {file = "mypy-0.940-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:51be997c1922e2b7be514a5215d1e1799a40832c0a0dee325ba8794f2c48818f"}, - {file = "mypy-0.940-cp37-cp37m-win_amd64.whl", hash = "sha256:628f5513268ebbc563750af672ccba5eef7f92d2d90154233edd498dfb98ca4e"}, - {file = "mypy-0.940-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:68038d514ae59d5b2f326be502a359160158d886bd153fc2489dbf7a03c44c96"}, - {file = "mypy-0.940-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2fa5f2d597478ccfe1f274f8da2f50ea1e63da5a7ae2342c5b3b2f3e57ec340"}, - {file = "mypy-0.940-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b1a116c451b41e35afc09618f454b5c2704ba7a4e36f9ff65014fef26bb6075b"}, - {file = "mypy-0.940-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f66f2309cdbb07e95e60e83fb4a8272095bd4ea6ee58bf9a70d5fb304ec3e3f"}, - {file = "mypy-0.940-cp38-cp38-win_amd64.whl", hash = "sha256:3ac14949677ae9cb1adc498c423b194ad4d25b13322f6fe889fb72b664c79121"}, - {file = "mypy-0.940-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6eab2bcc2b9489b7df87d7c20743b66d13254ad4d6430e1dfe1a655d51f0933d"}, - {file = "mypy-0.940-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0b52778a018559a256c819ee31b2e21e10b31ddca8705624317253d6d08dbc35"}, - {file = "mypy-0.940-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d9d7647505bf427bc7931e8baf6cacf9be97e78a397724511f20ddec2a850752"}, - {file = "mypy-0.940-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a0e5657ccaedeb5fdfda59918cc98fc6d8a8e83041bc0cec347a2ab6915f9998"}, - {file = "mypy-0.940-cp39-cp39-win_amd64.whl", hash = "sha256:83f66190e3c32603217105913fbfe0a3ef154ab6bbc7ef2c989f5b2957b55840"}, - {file = "mypy-0.940-py3-none-any.whl", hash = "sha256:a168da06eccf51875fdff5f305a47f021f23f300e2b89768abdac24538b1f8ec"}, - {file = "mypy-0.940.tar.gz", hash = "sha256:71bec3d2782d0b1fecef7b1c436253544d81c1c0e9ca58190aed9befd8f081c5"}, + {file = "mypy-0.950-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf9c261958a769a3bd38c3e133801ebcd284ffb734ea12d01457cb09eacf7d7b"}, + {file = "mypy-0.950-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5b5bd0ffb11b4aba2bb6d31b8643902c48f990cc92fda4e21afac658044f0c0"}, + {file = "mypy-0.950-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e7647df0f8fc947388e6251d728189cfadb3b1e558407f93254e35abc026e22"}, + {file = "mypy-0.950-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eaff8156016487c1af5ffa5304c3e3fd183edcb412f3e9c72db349faf3f6e0eb"}, + {file = "mypy-0.950-cp310-cp310-win_amd64.whl", hash = "sha256:563514c7dc504698fb66bb1cf897657a173a496406f1866afae73ab5b3cdb334"}, + {file = "mypy-0.950-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dd4d670eee9610bf61c25c940e9ade2d0ed05eb44227275cce88701fee014b1f"}, + {file = "mypy-0.950-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca75ecf2783395ca3016a5e455cb322ba26b6d33b4b413fcdedfc632e67941dc"}, + {file = "mypy-0.950-cp36-cp36m-win_amd64.whl", hash = "sha256:6003de687c13196e8a1243a5e4bcce617d79b88f83ee6625437e335d89dfebe2"}, + {file = "mypy-0.950-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4c653e4846f287051599ed8f4b3c044b80e540e88feec76b11044ddc5612ffed"}, + {file = "mypy-0.950-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e19736af56947addedce4674c0971e5dceef1b5ec7d667fe86bcd2b07f8f9075"}, + {file = "mypy-0.950-cp37-cp37m-win_amd64.whl", hash = "sha256:ef7beb2a3582eb7a9f37beaf38a28acfd801988cde688760aea9e6cc4832b10b"}, + {file = "mypy-0.950-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0112752a6ff07230f9ec2f71b0d3d4e088a910fdce454fdb6553e83ed0eced7d"}, + {file = "mypy-0.950-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee0a36edd332ed2c5208565ae6e3a7afc0eabb53f5327e281f2ef03a6bc7687a"}, + {file = "mypy-0.950-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77423570c04aca807508a492037abbd72b12a1fb25a385847d191cd50b2c9605"}, + {file = "mypy-0.950-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5ce6a09042b6da16d773d2110e44f169683d8cc8687e79ec6d1181a72cb028d2"}, + {file = "mypy-0.950-cp38-cp38-win_amd64.whl", hash = "sha256:5b231afd6a6e951381b9ef09a1223b1feabe13625388db48a8690f8daa9b71ff"}, + {file = "mypy-0.950-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0384d9f3af49837baa92f559d3fa673e6d2652a16550a9ee07fc08c736f5e6f8"}, + {file = "mypy-0.950-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1fdeb0a0f64f2a874a4c1f5271f06e40e1e9779bf55f9567f149466fc7a55038"}, + {file = "mypy-0.950-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:61504b9a5ae166ba5ecfed9e93357fd51aa693d3d434b582a925338a2ff57fd2"}, + {file = "mypy-0.950-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a952b8bc0ae278fc6316e6384f67bb9a396eb30aced6ad034d3a76120ebcc519"}, + {file = "mypy-0.950-cp39-cp39-win_amd64.whl", hash = "sha256:eaea21d150fb26d7b4856766e7addcf929119dd19fc832b22e71d942835201ef"}, + {file = "mypy-0.950-py3-none-any.whl", hash = "sha256:a4d9898f46446bfb6405383b57b96737dcfd0a7f25b748e78ef3e8c576bba3cb"}, + {file = "mypy-0.950.tar.gz", hash = "sha256:1b333cfbca1762ff15808a0ef4f71b5d3eed8528b23ea1c3fb50543c867d68de"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, @@ -1304,8 +1720,8 @@ passlib = [ {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, ] platformdirs = [ - {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, - {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -1372,23 +1788,28 @@ pydantic = [ {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"}, ] pyhumps = [ - {file = "pyhumps-3.5.3-py3-none-any.whl", hash = "sha256:8d7e9865d6ddb6e64a2e97d951b78b5cc827d3d66cda1297310fc83b2ddf51dc"}, - {file = "pyhumps-3.5.3.tar.gz", hash = "sha256:0ecf7fee84503b45afdd3841ec769b529d32dfaed855e07046ff8babcc0ab831"}, + {file = "pyhumps-3.7.1-py3-none-any.whl", hash = "sha256:c6f2d833f2c7afae039d71b7dc0aba5412ae5b8c8c33d4a208c1d412de17229e"}, + {file = "pyhumps-3.7.1.tar.gz", hash = "sha256:5616f0afdbc73ef479fa9999f4abdcb336a0232707ff1a0b86e29fc9339e18da"}, ] pylint = [ - {file = "pylint-2.12.2-py3-none-any.whl", hash = "sha256:daabda3f7ed9d1c60f52d563b1b854632fd90035bcf01443e234d3dc794e3b74"}, - {file = "pylint-2.12.2.tar.gz", hash = "sha256:9d945a73640e1fec07ee34b42f5669b770c759acd536ec7b16d7e4b87a9c9ff9"}, + {file = "pylint-2.13.9-py3-none-any.whl", hash = "sha256:705c620d388035bdd9ff8b44c5bcdd235bfb49d276d488dd2c8ff1736aa42526"}, + {file = "pylint-2.13.9.tar.gz", hash = "sha256:095567c96e19e6f57b5b907e67d265ff535e588fe26b12b5ebe1fc5645b2c731"}, ] pylint-pytest = [ {file = "pylint_pytest-1.1.2-py2.py3-none-any.whl", hash = "sha256:fb20ef318081cee3d5febc631a7b9c40fa356b05e4f769d6e60a337e58c8879b"}, ] pyparsing = [ - {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, - {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pytest = [ - {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, - {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, + {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, + {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, +] +pytest-asyncio = [ + {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"}, + {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"}, + {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"}, ] pytest-cov = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, @@ -1406,8 +1827,8 @@ python-dateutil = [ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-dotenv = [ - {file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"}, - {file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"}, + {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, + {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, ] python-jose = [ {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, @@ -1455,6 +1876,10 @@ requests = [ {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, ] +rfc3986 = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] rsa = [ {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, @@ -1468,78 +1893,74 @@ sniffio = [ {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, ] sqlalchemy = [ - {file = "SQLAlchemy-1.4.31-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c3abc34fed19fdeaead0ced8cf56dd121f08198008c033596aa6aae7cc58f59f"}, - {file = "SQLAlchemy-1.4.31-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8d0949b11681380b4a50ac3cd075e4816afe9fa4a8c8ae006c1ca26f0fa40ad8"}, - {file = "SQLAlchemy-1.4.31-cp27-cp27m-win32.whl", hash = "sha256:f3b7ec97e68b68cb1f9ddb82eda17b418f19a034fa8380a0ac04e8fe01532875"}, - {file = "SQLAlchemy-1.4.31-cp27-cp27m-win_amd64.whl", hash = "sha256:81f2dd355b57770fdf292b54f3e0a9823ec27a543f947fa2eb4ec0df44f35f0d"}, - {file = "SQLAlchemy-1.4.31-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4ad31cec8b49fd718470328ad9711f4dc703507d434fd45461096da0a7135ee0"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:05fa14f279d43df68964ad066f653193187909950aa0163320b728edfc400167"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dccff41478050e823271642837b904d5f9bda3f5cf7d371ce163f00a694118d6"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57205844f246bab9b666a32f59b046add8995c665d9ecb2b7b837b087df90639"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea8210090a816d48a4291a47462bac750e3bc5c2442e6d64f7b8137a7c3f9ac5"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-win32.whl", hash = "sha256:2e216c13ecc7fcdcbb86bb3225425b3ed338e43a8810c7089ddb472676124b9b"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-win_amd64.whl", hash = "sha256:e3a86b59b6227ef72ffc10d4b23f0fe994bef64d4667eab4fb8cd43de4223bec"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2fd4d3ca64c41dae31228b80556ab55b6489275fb204827f6560b65f95692cf3"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f22c040d196f841168b1456e77c30a18a3dc16b336ddbc5a24ce01ab4e95ae0"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c0c7171aa5a57e522a04a31b84798b6c926234cb559c0939840c3235cf068813"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d046a9aeba9bc53e88a41e58beb72b6205abb9a20f6c136161adf9128e589db5"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-win32.whl", hash = "sha256:d86132922531f0dc5a4f424c7580a472a924dd737602638e704841c9cb24aea2"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-win_amd64.whl", hash = "sha256:ca68c52e3cae491ace2bf39b35fef4ce26c192fd70b4cd90f040d419f70893b5"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:cf2cd387409b12d0a8b801610d6336ee7d24043b6dd965950eaec09b73e7262f"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb4b15fb1f0aafa65cbdc62d3c2078bea1ceecbfccc9a1f23a2113c9ac1191fa"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c317ddd7c586af350a6aef22b891e84b16bff1a27886ed5b30f15c1ed59caeaa"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c7ed6c69debaf6198fadb1c16ae1253a29a7670bbf0646f92582eb465a0b999"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-win32.whl", hash = "sha256:6a01ec49ca54ce03bc14e10de55dfc64187a2194b3b0e5ac0fdbe9b24767e79e"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-win_amd64.whl", hash = "sha256:330eb45395874cc7787214fdd4489e2afb931bc49e0a7a8f9cd56d6e9c5b1639"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:5e9c7b3567edbc2183607f7d9f3e7e89355b8f8984eec4d2cd1e1513c8f7b43f"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de85c26a5a1c72e695ab0454e92f60213b4459b8d7c502e0be7a6369690eeb1a"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:975f5c0793892c634c4920057da0de3a48bbbbd0a5c86f5fcf2f2fedf41b76da"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5c20c8415173b119762b6110af64448adccd4d11f273fb9f718a9865b88a99c"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-win32.whl", hash = "sha256:b35dca159c1c9fa8a5f9005e42133eed82705bf8e243da371a5e5826440e65ca"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-win_amd64.whl", hash = "sha256:b7b20c88873675903d6438d8b33fba027997193e274b9367421e610d9da76c08"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:85e4c244e1de056d48dae466e9baf9437980c19fcde493e0db1a0a986e6d75b4"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79e73d5ee24196d3057340e356e6254af4d10e1fc22d3207ea8342fc5ffb977"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:15a03261aa1e68f208e71ae3cd845b00063d242cbf8c87348a0c2c0fc6e1f2ac"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ddc5e5ccc0160e7ad190e5c61eb57560f38559e22586955f205e537cda26034"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-win32.whl", hash = "sha256:289465162b1fa1e7a982f8abe59d26a8331211cad4942e8031d2b7db1f75e649"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-win_amd64.whl", hash = "sha256:9e4fb2895b83993831ba2401b6404de953fdbfa9d7d4fa6a4756294a83bbc94f"}, - {file = "SQLAlchemy-1.4.31.tar.gz", hash = "sha256:582b59d1e5780a447aada22b461e50b404a9dc05768da1d87368ad8190468418"}, + {file = "SQLAlchemy-1.4.36-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:81e53bd383c2c33de9d578bfcc243f559bd3801a0e57f2bcc9a943c790662e0c"}, + {file = "SQLAlchemy-1.4.36-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6e1fe00ee85c768807f2a139b83469c1e52a9ffd58a6eb51aa7aeb524325ab18"}, + {file = "SQLAlchemy-1.4.36-cp27-cp27m-win32.whl", hash = "sha256:d57ac32f8dc731fddeb6f5d1358b4ca5456e72594e664769f0a9163f13df2a31"}, + {file = "SQLAlchemy-1.4.36-cp27-cp27m-win_amd64.whl", hash = "sha256:fca8322e04b2dde722fcb0558682740eebd3bd239bea7a0d0febbc190e99dc15"}, + {file = "SQLAlchemy-1.4.36-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:53d2d9ee93970c969bc4e3c78b1277d7129554642f6ffea039c282c7dc4577bc"}, + {file = "SQLAlchemy-1.4.36-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:f0394a3acfb8925db178f7728adb38c027ed7e303665b225906bfa8099dc1ce8"}, + {file = "SQLAlchemy-1.4.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c606d8238feae2f360b8742ffbe67741937eb0a05b57f536948d198a3def96"}, + {file = "SQLAlchemy-1.4.36-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8d07fe2de0325d06e7e73281e9a9b5e259fbd7cbfbe398a0433cbb0082ad8fa7"}, + {file = "SQLAlchemy-1.4.36-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5041474dcab7973baa91ec1f3112049a9dd4652898d6a95a6a895ff5c58beb6b"}, + {file = "SQLAlchemy-1.4.36-cp310-cp310-win32.whl", hash = "sha256:be094460930087e50fd08297db9d7aadaed8408ad896baf758e9190c335632da"}, + {file = "SQLAlchemy-1.4.36-cp310-cp310-win_amd64.whl", hash = "sha256:64d796e9af522162f7f2bf7a3c5531a0a550764c426782797bbeed809d0646c5"}, + {file = "SQLAlchemy-1.4.36-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:a0ae3aa2e86a4613f2d4c49eb7da23da536e6ce80b2bfd60bbb2f55fc02b0b32"}, + {file = "SQLAlchemy-1.4.36-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d50cb71c1dbed70646d521a0975fb0f92b7c3f84c61fa59e07be23a1aaeecfc"}, + {file = "SQLAlchemy-1.4.36-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:16abf35af37a3d5af92725fc9ec507dd9e9183d261c2069b6606d60981ed1c6e"}, + {file = "SQLAlchemy-1.4.36-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5864a83bd345871ad9699ce466388f836db7572003d67d9392a71998092210e3"}, + {file = "SQLAlchemy-1.4.36-cp36-cp36m-win32.whl", hash = "sha256:fbf8c09fe9728168f8cc1b40c239eab10baf9c422c18be7f53213d70434dea43"}, + {file = "SQLAlchemy-1.4.36-cp36-cp36m-win_amd64.whl", hash = "sha256:6e859fa96605027bd50d8e966db1c4e1b03e7b3267abbc4b89ae658c99393c58"}, + {file = "SQLAlchemy-1.4.36-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:166a3887ec355f7d2f12738f7fa25dc8ac541867147a255f790f2f41f614cb44"}, + {file = "SQLAlchemy-1.4.36-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e885548da361aa3f8a9433db4cfb335b2107e533bf314359ae3952821d84b3e"}, + {file = "SQLAlchemy-1.4.36-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5c90ef955d429966d84326d772eb34333178737ebb669845f1d529eb00c75e72"}, + {file = "SQLAlchemy-1.4.36-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a052bd9f53004f8993c624c452dfad8ec600f572dd0ed0445fbe64b22f5570e"}, + {file = "SQLAlchemy-1.4.36-cp37-cp37m-win32.whl", hash = "sha256:dce3468bf1fc12374a1a732c9efd146ce034f91bb0482b602a9311cb6166a920"}, + {file = "SQLAlchemy-1.4.36-cp37-cp37m-win_amd64.whl", hash = "sha256:6cb4c4f57a20710cea277edf720d249d514e587f796b75785ad2c25e1c0fed26"}, + {file = "SQLAlchemy-1.4.36-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e74ce103b81c375c3853b436297952ef8d7863d801dcffb6728d01544e5191b5"}, + {file = "SQLAlchemy-1.4.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b20c4178ead9bc398be479428568ff31b6c296eb22e75776273781a6551973f"}, + {file = "SQLAlchemy-1.4.36-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:af2587ae11400157753115612d6c6ad255143efba791406ad8a0cbcccf2edcb3"}, + {file = "SQLAlchemy-1.4.36-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83cf3077712be9f65c9aaa0b5bc47bc1a44789fd45053e2e3ecd59ff17c63fe9"}, + {file = "SQLAlchemy-1.4.36-cp38-cp38-win32.whl", hash = "sha256:ce20f5da141f8af26c123ebaa1b7771835ca6c161225ce728962a79054f528c3"}, + {file = "SQLAlchemy-1.4.36-cp38-cp38-win_amd64.whl", hash = "sha256:316c7e5304dda3e3ad711569ac5d02698bbc71299b168ac56a7076b86259f7ea"}, + {file = "SQLAlchemy-1.4.36-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:f522214f6749bc073262529c056f7dfd660f3b5ec4180c5354d985eb7219801e"}, + {file = "SQLAlchemy-1.4.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ecac4db8c1aa4a269f5829df7e706639a24b780d2ac46b3e485cbbd27ec0028"}, + {file = "SQLAlchemy-1.4.36-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b3db741beaa983d4cbf9087558620e7787106319f7e63a066990a70657dd6b35"}, + {file = "SQLAlchemy-1.4.36-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ec89bf98cc6a0f5d1e28e3ad28e9be6f3b4bdbd521a4053c7ae8d5e1289a8a1"}, + {file = "SQLAlchemy-1.4.36-cp39-cp39-win32.whl", hash = "sha256:e12532c4d3f614678623da5d852f038ace1f01869b89f003ed6fe8c793f0c6a3"}, + {file = "SQLAlchemy-1.4.36-cp39-cp39-win_amd64.whl", hash = "sha256:cb441ca461bf97d00877b607f132772644b623518b39ced54da433215adce691"}, + {file = "SQLAlchemy-1.4.36.tar.gz", hash = "sha256:64678ac321d64a45901ef2e24725ec5e783f1f4a588305e196431447e7ace243"}, ] sqlalchemy-utils = [ {file = "SQLAlchemy-Utils-0.38.2.tar.gz", hash = "sha256:9e01d6d3fb52d3926fcd4ea4a13f3540701b751aced0316bff78264402c2ceb4"}, {file = "SQLAlchemy_Utils-0.38.2-py3-none-any.whl", hash = "sha256:622235b1598f97300e4d08820ab024f5219c9a6309937a8b908093f487b4ba54"}, ] sqlalchemy2-stubs = [ - {file = "sqlalchemy2-stubs-0.0.2a20.tar.gz", hash = "sha256:3e96a5bb7d46a368c780ba57dcf2afbe2d3efdd75f7724ae7a859df0b0625f38"}, - {file = "sqlalchemy2_stubs-0.0.2a20-py3-none-any.whl", hash = "sha256:da31d0e30a2af2e5ad83dbce5738543a9f488089774f506de5ec7d28d425a202"}, + {file = "sqlalchemy2-stubs-0.0.2a22.tar.gz", hash = "sha256:31288db647bbdd411ad1e22da39a10ebe211bdcfe2efef24bcebea05abc28dd4"}, + {file = "sqlalchemy2_stubs-0.0.2a22-py3-none-any.whl", hash = "sha256:b9b907c3555d0b11bb8d738b788be478ce3871174839171d0d49aba5d0785016"}, ] starlette = [ - {file = "starlette-0.17.1-py3-none-any.whl", hash = "sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050"}, - {file = "starlette-0.17.1.tar.gz", hash = "sha256:57eab3cc975a28af62f6faec94d355a410634940f10b30d68d31cb5ec1b44ae8"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, + {file = "starlette-0.19.1-py3-none-any.whl", hash = "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf"}, + {file = "starlette-0.19.1.tar.gz", hash = "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7"}, ] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] types-passlib = [ - {file = "types-passlib-1.7.0.tar.gz", hash = "sha256:b069e428b601216e7220f5d3972c57706d85bdf2cd715be28c2a31ae4e5deaec"}, - {file = "types_passlib-1.7.0-py3-none-any.whl", hash = "sha256:e7d09757cd56343806cba44a1857809c0e594294badd83404c2138349b0ea8ec"}, + {file = "types-passlib-1.7.5.tar.gz", hash = "sha256:810ce820882a900429b2cbe6554851182370337c7246b0e0728ff4145db0edcf"}, + {file = "types_passlib-1.7.5-py3-none-any.whl", hash = "sha256:8366c5e31bbff65c0a6d1a0f10e84fba567797680c643b485b072691bc0908db"}, ] typing-extensions = [ - {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, - {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, + {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, + {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, ] urllib3 = [ {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, ] uvicorn = [ - {file = "uvicorn-0.15.0-py3-none-any.whl", hash = "sha256:17f898c64c71a2640514d4089da2689e5db1ce5d4086c2d53699bf99513421c1"}, - {file = "uvicorn-0.15.0.tar.gz", hash = "sha256:d9a3c0dd1ca86728d3e235182683b4cf94cd53a867c288eaeca80ee781b2caff"}, + {file = "uvicorn-0.17.6-py3-none-any.whl", hash = "sha256:19e2a0e96c9ac5581c01eb1a79a7d2f72bb479691acd2b8921fce48ed5b961a6"}, + {file = "uvicorn-0.17.6.tar.gz", hash = "sha256:5180f9d059611747d841a4a4c4ab675edf54c8489e97f96d0583ee90ac3bfc23"}, ] uvloop = [ {file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"}, @@ -1560,58 +1981,58 @@ uvloop = [ {file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"}, ] watchgod = [ - {file = "watchgod-0.8-py3-none-any.whl", hash = "sha256:339c2cfede1ccc1e277bbf5e82e42886f3c80801b01f45ab10d9461c4118b5eb"}, - {file = "watchgod-0.8.tar.gz", hash = "sha256:29a1d8f25e1721ddb73981652ca318c47387ffb12ec4171ddd7b9d01540033b1"}, + {file = "watchgod-0.8.2-py3-none-any.whl", hash = "sha256:2f3e8137d98f493ff58af54ea00f4d1433a6afe2ed08ab331a657df468c6bfce"}, + {file = "watchgod-0.8.2.tar.gz", hash = "sha256:cb11ff66657befba94d828e3b622d5fb76f22fbda1376f355f3e6e51e97d9450"}, ] websockets = [ - {file = "websockets-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5396710f86a306cf52f87fd8ea594a0e894ba0cc5a36059eaca3a477dc332aa"}, - {file = "websockets-10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b22bdc795e62e71118b63e14a08bacfa4f262fd2877de7e5b950f5ac16b0348f"}, - {file = "websockets-10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b04270b5613f245ec84bb2c6a482a9d009aefad37c0575f6cda8499125d5d5c"}, - {file = "websockets-10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5c335dc0e7dc271ef36df3f439868b3c790775f345338c2f61a562f1074187b"}, - {file = "websockets-10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a009eb551c46fd79737791c0c833fc0e5b56bcd1c3057498b262d660b92e9cd"}, - {file = "websockets-10.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a10c0c1ee02164246f90053273a42d72a3b2452a7e7486fdae781138cf7fbe2d"}, - {file = "websockets-10.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7b38a5c9112e3dbbe45540f7b60c5204f49b3cb501b40950d6ab34cd202ab1d0"}, - {file = "websockets-10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2aa9b91347ecd0412683f28aabe27f6bad502d89bd363b76e0a3508b1596402e"}, - {file = "websockets-10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b7fe45ae43ac814beb8ca09d6995b56800676f2cfa8e23f42839dc69bba34a42"}, - {file = "websockets-10.2-cp310-cp310-win32.whl", hash = "sha256:cef40a1b183dcf39d23b392e9dd1d9b07ab9c46aadf294fff1350fb79146e72b"}, - {file = "websockets-10.2-cp310-cp310-win_amd64.whl", hash = "sha256:c21a67ab9a94bd53e10bba21912556027fea944648a09e6508415ad14e37c325"}, - {file = "websockets-10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb316b87cbe3c0791c2ad92a5a36bf6adc87c457654335810b25048c1daa6fd5"}, - {file = "websockets-10.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f14bd10e170abc01682a9f8b28b16e6f20acf6175945ef38db6ffe31b0c72c3f"}, - {file = "websockets-10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fa35c5d1830d0fb7b810324e9eeab9aa92e8f273f11fdbdc0741dcded6d72b9f"}, - {file = "websockets-10.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:71a4491cfe7a9f18ee57d41163cb6a8a3fa591e0f0564ca8b0ed86b2a30cced4"}, - {file = "websockets-10.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6193bbc1ee63aadeb9a4d81de0e19477401d150d506aee772d8380943f118186"}, - {file = "websockets-10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8beac786a388bb99a66c3be4ab0fb38273c0e3bc17f612a4e0a47c4fc8b9c045"}, - {file = "websockets-10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c67d9cacb3f6537ca21e9b224d4fd08481538e43bcac08b3d93181b0816def39"}, - {file = "websockets-10.2-cp37-cp37m-win32.whl", hash = "sha256:a03a25d95cc7400bd4d61a63460b5d85a7761c12075ee2f51de1ffe73aa593d3"}, - {file = "websockets-10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f8296b8408ec6853b26771599990721a26403e62b9de7e50ac0a056772ac0b5e"}, - {file = "websockets-10.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7bb9d8a6beca478c7e9bdde0159bd810cc1006ad6a7cb460533bae39da692ca2"}, - {file = "websockets-10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:05f6e9757017270e7a92a2975e2ae88a9a582ffc4629086fd6039aa80e99cd86"}, - {file = "websockets-10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1c9031e90ebfc486e9cdad532b94004ade3aa39a31d3c46c105bb0b579cd2490"}, - {file = "websockets-10.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82bc33db6d8309dc27a3bee11f7da2288ad925fcbabc2a4bb78f7e9c56249baf"}, - {file = "websockets-10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:24b879ba7db12bb525d4e58089fcbe6a3df3ce4666523183654170e86d372cbe"}, - {file = "websockets-10.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cf931c33db9c87c53d009856045dd524e4a378445693382a920fa1e0eb77c36c"}, - {file = "websockets-10.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:669e54228a4d9457abafed27cbf0e2b9f401445c4dfefc12bf8e4db9751703b8"}, - {file = "websockets-10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:bffc65442dd35c473ca9790a3fa3ba06396102a950794f536783f4b8060af8dd"}, - {file = "websockets-10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d4d110a84b63c5cfdd22485acc97b8b919aefeecd6300c0c9d551e055b9a88ea"}, - {file = "websockets-10.2-cp38-cp38-win32.whl", hash = "sha256:117383d0a17a0dda349f7a8790763dde75c1508ff8e4d6e8328b898b7df48397"}, - {file = "websockets-10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b66421f9f13d4df60cd48ab977ed2c2b6c9147ae1a33caf5a9f46294422fda1"}, - {file = "websockets-10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ac081aa0307f263d63c5ff0727935c736c8dad51ddf2dc9f5d0c4759842aefaa"}, - {file = "websockets-10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b4059e2ccbe6587b6dc9a01db5fc49ead9a884faa4076eea96c5ec62cb32f42a"}, - {file = "websockets-10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9ca2ca05a4c29179f06cf6727b45dba5d228da62623ec9df4184413d8aae6cb9"}, - {file = "websockets-10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97950c7c844ec6f8d292440953ae18b99e3a6a09885e09d20d5e7ecd9b914cf8"}, - {file = "websockets-10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:98f57b3120f8331cd7440dbe0e776474f5e3632fdaa474af1f6b754955a47d71"}, - {file = "websockets-10.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a72b92f96e5e540d5dda99ee3346e199ade8df63152fa3c737260da1730c411f"}, - {file = "websockets-10.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:038afef2a05893578d10dadbdbb5f112bd115c46347e1efe99f6a356ff062138"}, - {file = "websockets-10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f09f46b1ff6d09b01c7816c50bd1903cf7d02ebbdb63726132717c2fcda835d5"}, - {file = "websockets-10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2349fa81b6b959484bb2bda556ccb9eb70ba68987646a0f8a537a1a18319fb03"}, - {file = "websockets-10.2-cp39-cp39-win32.whl", hash = "sha256:bef03a51f9657fb03d8da6ccd233fe96e04101a852f0ffd35f5b725b28221ff3"}, - {file = "websockets-10.2-cp39-cp39-win_amd64.whl", hash = "sha256:1c1f3b18c8162e3b09761d0c6a0305fd642934202541cc511ef972cb9463261e"}, - {file = "websockets-10.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5a38a0175ae82e4a8c4bac29fc01b9ee26d7d5a614e5ee11e7813c68a7d938ce"}, - {file = "websockets-10.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6e56606842bb24e16e36ae7eb308d866b4249cf0be8f63b212f287eeb76b124"}, - {file = "websockets-10.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0f73cb2526d6da268e86977b2c4b58f2195994e53070fe567d5487c6436047e6"}, - {file = "websockets-10.2-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cd02f36d37e503aca88ab23cc0a1a0e92a263d37acf6331521eb38040dcf77b"}, - {file = "websockets-10.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:56d48eebe9e39ce0d68701bce3b21df923aa05dcc00f9fd8300de1df31a7c07c"}, - {file = "websockets-10.2.tar.gz", hash = "sha256:8351c3c86b08156337b0e4ece0e3c5ec3e01fcd14e8950996832a23c99416098"}, + {file = "websockets-10.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978"}, + {file = "websockets-10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500"}, + {file = "websockets-10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b"}, + {file = "websockets-10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c"}, + {file = "websockets-10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8"}, + {file = "websockets-10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677"}, + {file = "websockets-10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e"}, + {file = "websockets-10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f"}, + {file = "websockets-10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47"}, + {file = "websockets-10.3-cp310-cp310-win32.whl", hash = "sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae"}, + {file = "websockets-10.3-cp310-cp310-win_amd64.whl", hash = "sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079"}, + {file = "websockets-10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916"}, + {file = "websockets-10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb"}, + {file = "websockets-10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79"}, + {file = "websockets-10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d"}, + {file = "websockets-10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98"}, + {file = "websockets-10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e"}, + {file = "websockets-10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6"}, + {file = "websockets-10.3-cp37-cp37m-win32.whl", hash = "sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1"}, + {file = "websockets-10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4"}, + {file = "websockets-10.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36"}, + {file = "websockets-10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69"}, + {file = "websockets-10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd"}, + {file = "websockets-10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2"}, + {file = "websockets-10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c"}, + {file = "websockets-10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e"}, + {file = "websockets-10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991"}, + {file = "websockets-10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442"}, + {file = "websockets-10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76"}, + {file = "websockets-10.3-cp38-cp38-win32.whl", hash = "sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559"}, + {file = "websockets-10.3-cp38-cp38-win_amd64.whl", hash = "sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d"}, + {file = "websockets-10.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094"}, + {file = "websockets-10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667"}, + {file = "websockets-10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731"}, + {file = "websockets-10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9"}, + {file = "websockets-10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680"}, + {file = "websockets-10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247"}, + {file = "websockets-10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af"}, + {file = "websockets-10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3"}, + {file = "websockets-10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8"}, + {file = "websockets-10.3-cp39-cp39-win32.whl", hash = "sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582"}, + {file = "websockets-10.3-cp39-cp39-win_amd64.whl", hash = "sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02"}, + {file = "websockets-10.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7"}, + {file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f"}, + {file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4"}, + {file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755"}, + {file = "websockets-10.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55"}, + {file = "websockets-10.3.tar.gz", hash = "sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4"}, ] wrapt = [ {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, @@ -1666,3 +2087,77 @@ wrapt = [ {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, ] +yarl = [ + {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, + {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, + {file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"}, + {file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"}, + {file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"}, + {file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"}, + {file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"}, + {file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"}, + {file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"}, + {file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"}, + {file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"}, + {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"}, + {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"}, + {file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"}, + {file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"}, + {file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"}, + {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"}, + {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"}, + {file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"}, + {file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"}, + {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"}, + {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"}, +] diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 3c714a732..0c672411d 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -9,16 +9,16 @@ license = "MIT" python = "^3.10" # Alembic: Database migrations extension for SQLAlchemy -alembic = "1.7.6" +alembic = "1.7.7" # Environs: simplified environment variable parsing environs = "9.5.0" # FastAPI: API framework -fastapi = "0.74.1" +fastapi = "0.78.0" # MariaDB: Python MariaDB connector -mariadb = "1.0.10" +mariadb = "1.0.11" # Hash passwords passlib = { "version" = "1.7.4", extras = ["bcrypt"] } @@ -27,7 +27,7 @@ passlib = { "version" = "1.7.4", extras = ["bcrypt"] } python-jose = { "version" = "3.3.0", extras = ["cryptography"] } # Humps: Convert strings (and dictionary keys) between snake case, camel case and pascal case in Python -pyhumps = "3.5.3" +pyhumps = "3.7.1" # OAuth2 form data python-multipart = "0.0.5" @@ -36,33 +36,46 @@ python-multipart = "0.0.5" requests = "2.27.1" # SQLAlchemy: ORM and database toolkit -SQLAlchemy = "1.4.31" +SQLAlchemy = "1.4.36" # SQLAlchemy-Utils: Various utility functions and datatypes for SQLAlchemy sqlalchemy-utils = "0.38.2" # Uvicorn: ASGI web server implementation -uvicorn = { "version" = ">=0.12.0, < 0.16.0", extras = ["standard"] } +uvicorn = { "version" = ">=0.12.0,<0.18.0", extras = ["standard"] } +aiohttp = "^3.8.1" + +# AioSQLite: async connector for SQLite +aiosqlite = "^0.17.0" + +# Asyncmy: async connector for MariaDB +asyncmy = "^0.2.5" + +# Httpx: send async HTTP requests +httpx = "^0.22.0" [tool.poetry.dev-dependencies] # Coverage: generate code coverage reports -coverage = { "version" = "6.3.1", extras = ["toml"] } +coverage = { "version" = "6.3.3", extras = ["toml"] } # faker: Generate dummy data -faker = "13.3.1" +faker = "13.11.1" # Mypy: check type usage in code -mypy = "0.940" +mypy = "0.950" # Pylint: Python linter -pylint = "2.12.2" +pylint = "2.13.9" # Pylint-Pytest: A Pylint plugin to suppress pytest-related false positives. pylint-pytest = "1.1.2" # Pytest: Python testing framework # (more advanced than the built-in unittest module) -pytest = "7.0.1" +pytest = "7.1.2" + +# Pytest-asyncio: plugin for pytest to support async tests & fixtures +pytest-asyncio = "^0.18.3" # Pytest-cov: coverage plugin for pytest pytest-cov = "3.0.0" @@ -74,10 +87,10 @@ pytest-env = "0.6.2" pytest-mock = "3.7.0" # Sqlalchemy-stubs: type hints for sqlalchemy -sqlalchemy2-stubs="0.0.2a20" +sqlalchemy2-stubs="0.0.2a22" # Types for the passlib library -types-passlib="1.7.0" +types-passlib="1.7.5" [tool.mypy] plugins = ["sqlalchemy.ext.mypy.plugin"] @@ -103,12 +116,26 @@ extension-pkg-whitelist = "pydantic" [tool.pylint.format] max-line-length = 120 +[tool.pylint.similarities] +min-similarity-lines=10 + [tool.pytest.ini_options] +asyncio_mode = "auto" filterwarnings = [ "ignore:.*The distutils package is deprecated:DeprecationWarning", ] env = [ "DB_USE_SQLITE = 1", + "GITHUB_CLIENT_ID = 25", + "GITHUB_CLIENT_SECRET = secret" +] + +[tool.coverage.run] +concurrency = [ + "greenlet" +] +include = [ + "src/*" ] [build-system] diff --git a/backend/settings.py b/backend/settings.py index 342f8bd35..d944137f9 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -33,23 +33,44 @@ ACCESS_TOKEN_EXPIRE_M: int = env.int("ACCESS_TOKEN_EXPIRE_M", 5) REFRESH_TOKEN_EXPIRE_M: int = env.int("REFRESH_TOKEN_EXPIRE_M", 2880) +"""GitHub OAuth""" +GITHUB_CLIENT_ID: str | None = env.str("GITHUB_CLIENT_ID", None) +GITHUB_CLIENT_SECRET: str | None = env.str("GITHUB_CLIENT_SECRET", None) + """Frontend""" FRONTEND_URL: str = env.str("FRONTEND_URL", "http://localhost:3000") +# Tally Form - ID's for specific questions & information @enum.unique class FormMapping(enum.Enum): - FIRST_NAME = "question_3ExXkL" - LAST_NAME = "question_nro6jL" - PREFERRED_NAME_OPTION = "question_w4K84o" - PREFERRED_NAME = "question_3jlya9" - EMAIL = "question_nW8NOQ" - PHONE_NUMBER = "question_mea6qo" - # CV = "question_wa26Qy" - STUDENT_COACH = "question_wz7qEE" + FIRST_NAME = env.str("FORM_FIRST_NAME", "question_3ExXkL") + LAST_NAME = env.str("FORM_LAST_NAME", "question_nro6jL") + PREFERRED_NAME_OPTION = env.str("FORM_PREFERRED_NAME_OPTION", "question_w4K84o") + PREFERRED_NAME = env.str("FORM_PREFERRED_NAME", "question_3jlya9") + EMAIL = env.str("FORM_EMAIL", "question_nW8NOQ") + PHONE_NUMBER = env.str("FORM_PHONE_NUMBER", "question_mea6qo") + STUDENT_COACH = env.str("FORM_STUDENT_COACH", "question_wz7qEE") + ROLES = env.str("FORM_ROLES", "question_3yJ6PW") + ALUMNI = env.str("FORM_ALUMNI", "question_n0exVQ") UNKNOWN = None # Returned when no specific question can be matched @classmethod def _missing_(cls, value: object) -> Any: return FormMapping.UNKNOWN + + +# Skills that should be added into the database when starting the API +REQUIRED_SKILLS = [ + "Front-end Developer", + "Back-end Developer", + "UX / UI Designer", + "Graphic Designer", + "Business Modeller", + "Storyteller", + "Marketer", + "Copywriter", + "Video Editor", + "Photographer" +] diff --git a/backend/src/app/app.py b/backend/src/app/app.py index 520777d06..a64031445 100644 --- a/backend/src/app/app.py +++ b/backend/src/app/app.py @@ -5,16 +5,18 @@ from starlette.middleware.cors import CORSMiddleware import settings -from src.database.engine import engine -from src.database.exceptions import PendingMigrationsException +from src.database.crud import skills as skills_crud +from src.database.engine import engine, DBSession from .exceptions import install_handlers from .routers import editions_router, login_router, skills_router from .routers.users.users import users_router - +from .utils.websockets import install_middleware # Main application +from ..database.exceptions import PendingMigrationsException + app = FastAPI( title="OSOC Team 3", - version="0.0.1" + version="3.0.0" ) # Add middleware @@ -25,6 +27,7 @@ allow_methods=["*"], allow_headers=["*"], ) +install_middleware(app) # Include all routers app.include_router(editions_router) @@ -37,13 +40,21 @@ @app.on_event('startup') -async def startup(): +async def init_database(): # pragma: no cover """ - Check if all migrations have been executed. If not refuse to start the app. + Create all tables and skills if they don't exist """ alembic_config: config.Config = config.Config('alembic.ini') alembic_script: script.ScriptDirectory = script.ScriptDirectory.from_config(alembic_config) - with engine.begin() as conn: - context: migration.MigrationContext = migration.MigrationContext.configure(conn) - if context.get_current_revision() != alembic_script.get_current_head(): + async with engine.begin() as conn: + revision: str = await conn.run_sync( + lambda sync_conn: migration.MigrationContext.configure(sync_conn).get_current_revision() + ) + alembic_head: str = alembic_script.get_current_head() + if revision != alembic_head: raise PendingMigrationsException('Pending migrations') + + async with DBSession() as conn: + for skill in settings.REQUIRED_SKILLS: + if await skills_crud.create_skill_if_not_present(conn, skill): + print(f"Created missing skill \"{skill}\"") diff --git a/backend/src/app/exceptions/crud.py b/backend/src/app/exceptions/crud.py new file mode 100644 index 000000000..8221e908e --- /dev/null +++ b/backend/src/app/exceptions/crud.py @@ -0,0 +1,6 @@ +class DuplicateInsertException(Exception): + """Exception raised when an element is inserted twice + + Args: + Exception (Exception): base Exception class + """ diff --git a/backend/src/app/exceptions/editions.py b/backend/src/app/exceptions/editions.py index c61f8d3fe..7468bab23 100644 --- a/backend/src/app/exceptions/editions.py +++ b/backend/src/app/exceptions/editions.py @@ -1,8 +1,2 @@ -class DuplicateInsertException(Exception): - """Exception raised when an element is inserted twice - - Args: - Exception (Exception): base Exception class - """ class ReadOnlyEditionException(Exception): """Exception raised when a read-only edition is being changed""" diff --git a/backend/src/app/exceptions/handlers.py b/backend/src/app/exceptions/handlers.py index a2c82293b..5ad0c55a7 100644 --- a/backend/src/app/exceptions/handlers.py +++ b/backend/src/app/exceptions/handlers.py @@ -7,15 +7,21 @@ from .authentication import ( ExpiredCredentialsException, InvalidCredentialsException, MissingPermissionsException, WrongTokenTypeException) -from .editions import DuplicateInsertException, ReadOnlyEditionException +from .crud import DuplicateInsertException +from .editions import ReadOnlyEditionException from .parsing import MalformedUUIDError -from .projects import StudentInConflictException, FailedToAddProjectRoleException -from .register import FailedToAddNewUserException +from .projects import StudentInConflictException, FailedToAddProjectRoleException, NoStrictlyPositiveNumberOfSlots +from .register import FailedToAddNewUserException, InvalidGitHubCode from .students_email import FailedToAddNewEmailException from .webhooks import WebhookProcessException +from .util import NotFound -def install_handlers(app: FastAPI): +# Pylint says there are too many local variables because of all the inner functions here +# however, we can't really change that so we'll just disable the warning because it doesn't make +# a lot of sense +# pylint: disable=R0914 +def install_handlers(app: FastAPI): # pragma: no cover """Install all custom exception handlers""" @app.exception_handler(ExpiredCredentialsException) @@ -33,6 +39,13 @@ def invalid_credentials(_request: Request, _exception: InvalidCredentialsExcepti headers={"WWW-Authenticate": "Bearer"}, ) + @app.exception_handler(InvalidGitHubCode) + def invalid_github_code(_request: Request, _exception: InvalidGitHubCode): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"message": str(_exception)} + ) + @app.exception_handler(MalformedUUIDError) def malformed_uuid_error(_request: Request, _exception: MalformedUUIDError): return JSONResponse( @@ -63,6 +76,13 @@ def sqlalchemy_exc_no_result_found(_request: Request, _exception: sqlalchemy.exc content={'message': 'Not Found'} ) + @app.exception_handler(NotFound) + def not_found(_request: Request, _exception: NotFound): + return JSONResponse( + status_code=status.HTTP_404_NOT_FOUND, + content={'message': 'Not Found'} + ) + @app.exception_handler(DuplicateInsertException) def duplicate_insert(_request: Request, _exception: DuplicateInsertException): return JSONResponse( @@ -106,6 +126,7 @@ async def wrong_token_type_exception(_request: Request, _exception: WrongTokenTy status_code=status.HTTP_401_UNAUTHORIZED, content={'message': 'You used the wrong token to access this resource.'} ) + @app.exception_handler(ReadOnlyEditionException) def read_only_edition_exception(_request: Request, _exception: ReadOnlyEditionException): return JSONResponse( @@ -119,3 +140,10 @@ def failed_to_add_new_email_exception(_request: Request, _exception: FailedToAdd status_code=status.HTTP_400_BAD_REQUEST, content={'message': 'Something went wrong while creating a new email'} ) + + @app.exception_handler(NoStrictlyPositiveNumberOfSlots) + def none_strict_postive_number_of_slots(_request: Request, _exception: NoStrictlyPositiveNumberOfSlots): + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content={'message': 'The amount of slots per role has to be a strictly positive integer'} + ) diff --git a/backend/src/app/exceptions/projects.py b/backend/src/app/exceptions/projects.py index 9377d1a4c..d1dffc67f 100644 --- a/backend/src/app/exceptions/projects.py +++ b/backend/src/app/exceptions/projects.py @@ -8,3 +8,7 @@ class FailedToAddProjectRoleException(Exception): """ Exception raised when a project_role can't be added for some reason """ + + +class NoStrictlyPositiveNumberOfSlots(Exception): + """Exception raised when roles aren't strictly positive""" diff --git a/backend/src/app/exceptions/register.py b/backend/src/app/exceptions/register.py index 457c86a94..a61a67b9d 100644 --- a/backend/src/app/exceptions/register.py +++ b/backend/src/app/exceptions/register.py @@ -2,3 +2,7 @@ class FailedToAddNewUserException(Exception): """ Exception raised when a new user can't be added """ + + +class InvalidGitHubCode(Exception): + """Exception raised when a GitHub auth code is invalid or has expired""" diff --git a/backend/src/app/exceptions/util.py b/backend/src/app/exceptions/util.py new file mode 100644 index 000000000..886c3eb56 --- /dev/null +++ b/backend/src/app/exceptions/util.py @@ -0,0 +1,2 @@ +class NotFound(Exception): + """Exception raised when the backend should return a 404 Not Found, but it will not be generated by sqlalchemy""" diff --git a/backend/src/app/logic/answers.py b/backend/src/app/logic/answers.py new file mode 100644 index 000000000..6ffa51ee6 --- /dev/null +++ b/backend/src/app/logic/answers.py @@ -0,0 +1,22 @@ +from src.database.models import Student +from src.app.schemas.answers import Questions, QuestionAndAnswer, File + + +async def gives_question_and_answers(student: Student) -> Questions: + """transfers the student questions into a return model of Questions""" + # return Questions(questions=student.questions) + q_and_as: list[QuestionAndAnswer] = [] + for question in student.questions: + answers: list[str] = [] + for answer in question.answers: + if answer.answer: + answers.append(answer.answer) + + files: list[File] = [] + for file in question.files: + files.append(File(filename=file.file_name, + mime_type=file.mime_type, url=file.url)) + + q_and_as.append(QuestionAndAnswer( + question=question.question, answers=answers, files=files)) + return Questions(q_and_a=q_and_as) diff --git a/backend/src/app/logic/editions.py b/backend/src/app/logic/editions.py index da3eb4e5a..05fb2bb69 100644 --- a/backend/src/app/logic/editions.py +++ b/backend/src/app/logic/editions.py @@ -1,16 +1,17 @@ -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession import src.database.crud.editions as crud_editions from src.app.schemas.editions import EditionBase, EditionList from src.database.models import Edition as EditionModel -def get_editions_page(db: Session, page: int) -> EditionList: +async def get_editions_page(db: AsyncSession, page: int) -> EditionList: """Get a paginated list of all editions.""" - return EditionList(editions=crud_editions.get_editions_page(db, page)) + editions_page = await crud_editions.get_editions_page(db, page) + return EditionList(editions=editions_page) -def get_edition_by_name(db: Session, edition_name: str) -> EditionModel: +async def get_edition_by_name(db: AsyncSession, edition_name: str) -> EditionModel: """Get a specific edition. Args: @@ -19,28 +20,20 @@ def get_edition_by_name(db: Session, edition_name: str) -> EditionModel: Returns: Edition: an edition. """ - return crud_editions.get_edition_by_name(db, edition_name) + return await crud_editions.get_edition_by_name(db, edition_name) -def create_edition(db: Session, edition: EditionBase) -> EditionModel: - """ Create a new edition. +async def create_edition(db: AsyncSession, edition: EditionBase) -> EditionModel: + """Create a new edition.""" + return await crud_editions.create_edition(db, edition) - Args: - db (Session): connection with the database. - - Returns: - Edition: the newly made edition object. - """ - return crud_editions.create_edition(db, edition) +async def delete_edition(db: AsyncSession, edition_name: str): + """Delete an existing edition.""" + await crud_editions.delete_edition(db, edition_name) -def delete_edition(db: Session, edition_name: str): - """Delete an existing edition. - Args: - db (Session): connection with the database. - edition_name (str): the name of the edition that needs to be deleted, if found. - - Returns: nothing - """ - crud_editions.delete_edition(db, edition_name) +async def patch_edition(db: AsyncSession, edition: EditionModel, readonly: bool) -> EditionModel: + """Edit an existing edition""" + await crud_editions.patch_edition(db, edition, readonly) + return edition diff --git a/backend/src/app/logic/invites.py b/backend/src/app/logic/invites.py index 028a0b121..a4e3f87cb 100644 --- a/backend/src/app/logic/invites.py +++ b/backend/src/app/logic/invites.py @@ -1,6 +1,6 @@ import base64 -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession import settings import src.database.crud.invites as crud @@ -9,22 +9,23 @@ from src.database.models import Edition, InviteLink as InviteLinkDB -def delete_invite_link(db: Session, invite_link: InviteLinkDB): +async def delete_invite_link(db: AsyncSession, invite_link: InviteLinkDB): """Delete an invite link from the database""" - crud.delete_invite_link(db, invite_link) + await crud.delete_invite_link(db, invite_link) -def get_pending_invites_page(db: Session, edition: Edition, page: int) -> InvitesLinkList: +async def get_pending_invites_page(db: AsyncSession, edition: Edition, page: int) -> InvitesLinkList: """Query the database for a list of invite links and wrap the result in a pydantic model""" - return InvitesLinkList(invite_links=crud.get_pending_invites_for_edition_page(db, edition, page)) + invite_page = await crud.get_pending_invites_for_edition_page(db, edition, page) + return InvitesLinkList(invite_links=invite_page) -def create_mailto_link(db: Session, edition: Edition, email_address: EmailAddress) -> NewInviteLink: +async def create_mailto_link(db: AsyncSession, edition: Edition, email_address: EmailAddress) -> NewInviteLink: """Add a new invite link into the database & return a mailto link for it""" # Create db entry, drop existing. - invite = crud.get_optional_invite_link_by_edition_and_email(db, edition, email_address.email) + invite = await crud.get_optional_invite_link_by_edition_and_email(db, edition, email_address.email) if invite is None: - invite = crud.create_invite_link(db, edition, email_address.email) + invite = await crud.create_invite_link(db, edition, email_address.email) # Add edition name & encode with base64 encoded_uuid = f"{invite.edition.name}/{invite.uuid}".encode("utf-8") @@ -33,7 +34,10 @@ def create_mailto_link(db: Session, edition: Edition, email_address: EmailAddres # Create endpoint for the user to click on link = f"{settings.FRONTEND_URL}/register/{encoded_link}" + with open('templates/invites.txt', 'r', encoding="utf-8") as file: + message = file.read().format(invite_link=link) + return NewInviteLink(mail_to=generate_mailto_string( recipient=email_address.email, subject=f"Open Summer Of Code {edition.year} invitation", - body=link + body=message ), invite_link=link) diff --git a/frontend/src/views/StudentsPage/StudentsPage.css b/backend/src/app/logic/oauth/__init__.py similarity index 100% rename from frontend/src/views/StudentsPage/StudentsPage.css rename to backend/src/app/logic/oauth/__init__.py diff --git a/backend/src/app/logic/oauth/github.py b/backend/src/app/logic/oauth/github.py new file mode 100644 index 000000000..7eaab1606 --- /dev/null +++ b/backend/src/app/logic/oauth/github.py @@ -0,0 +1,97 @@ +import aiohttp +from sqlalchemy.ext.asyncio import AsyncSession +from starlette import status + +from src.app.exceptions.register import InvalidGitHubCode +from src.database.crud.users import get_user_by_github_id +from src.database.models import User +from src.app.schemas.oauth.github import AccessTokenResponse, GitHubProfile +import settings + + +async def get_github_access_token(http_session: aiohttp.ClientSession, code: str) -> AccessTokenResponse: + """Get a user's GitHub access token""" + headers = { + # Explicitly request the V3 API as recommended in the docs: + # https://docs.github.com/en/rest/overview/resources-in-the-rest-api#current-version + "Accept": "application/vnd.github.v3+json" + } + + params = { + "client_id": settings.GITHUB_CLIENT_ID, + "client_secret": settings.GITHUB_CLIENT_SECRET, + "code": code + } + + token_response = await http_session.post( + "https://github.com/login/oauth/access_token", + headers=headers, + params=params + ) + + token_response_json = await token_response.json() + + # For some reason this endpoint responds with a 200 if something is wrong so we have to check + # the fields in the body + if "error" in token_response_json: + raise InvalidGitHubCode(token_response_json["error_description"]) + + return AccessTokenResponse(**token_response_json) + + +async def get_github_profile(http_session: aiohttp.ClientSession, access_token: str) -> GitHubProfile: + """Get a user's profile info used on GitHub""" + headers = { + "Authorization": f"Bearer {access_token}" + } + + profile = await http_session.get("https://api.github.com/user", headers=headers) + profile_json = await profile.json() + + assert profile.status == status.HTTP_200_OK, profile_json + + # Default to GH name if real name is not available + name = profile_json.get("name", None) or profile_json.get("login") + email = profile_json.get("email", None) + + # Email can be private, in which case we have to send another request + # to access all their other emails + if email is None: + user_emails = await http_session.get("https://api.github.com/user/emails", headers=headers) + user_emails_json = await user_emails.json() + + assert user_emails.status == status.HTTP_200_OK, user_emails_json + + # Find primary email + for private_email in user_emails_json: + if private_email["primary"]: + email = private_email["email"] + break + + # No primary email set, take the first email address from the list + # (no idea if this is possible, but better safe than sorry) + if email is None: + email = user_emails_json[0]["email"] + + return GitHubProfile(access_token=access_token, email=email, id=profile_json["id"], name=name) + + +async def get_github_id(http_session: aiohttp.ClientSession, access_token: str) -> int: + """Get a user's GitHub user id""" + headers = { + "Authorization": f"Bearer {access_token}" + } + + profile = await http_session.get("https://api.github.com/user", headers=headers) + profile_json = await profile.json() + + assert profile.status == status.HTTP_200_OK, profile_json + + return profile_json["id"] + + +async def get_user_by_github_code(http_session: aiohttp.ClientSession, db: AsyncSession, code: str) -> User: + """Find a User by their GitHub auth code""" + token_data = await get_github_access_token(http_session, code) + github_id = await get_github_id(http_session, token_data.access_token) + return await get_user_by_github_id(db, github_id) diff --git a/backend/src/app/logic/partners.py b/backend/src/app/logic/partners.py new file mode 100644 index 000000000..a10d4cd93 --- /dev/null +++ b/backend/src/app/logic/partners.py @@ -0,0 +1,17 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +import src.database.crud.partners as crud + +from src.database.models import Partner + + +async def get_or_create_partners_by_name(db: AsyncSession, names: list[str], commit: bool = True) -> list[Partner]: + """Return a list of partners, when a partner with the name does not exist, create it""" + partners: list[Partner] = [] + for partner_name in names: + partner = await crud.get_optional_partner_by_name(db, partner_name) + if partner is None: + partners.append(await crud.create_partner(db, partner_name, commit=commit)) + else: + partners.append(partner) + return partners diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index a733f143f..e17c08450 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -1,37 +1,91 @@ -from sqlalchemy.orm import Session +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from src.app.exceptions.projects import NoStrictlyPositiveNumberOfSlots +import src.app.logic.partners as partners_logic import src.database.crud.projects as crud from src.app.schemas.projects import ( - ProjectList, ConflictStudentList, InputProject, ConflictStudent, QueryParamsProjects + ProjectList, ConflictStudentList, InputProject, InputProjectRole, QueryParamsProjects, + ProjectRoleResponseList ) -from src.database.models import Edition, Project, User +from src.database.models import Edition, Project, ProjectRole, User -def get_project_list(db: Session, edition: Edition, search_params: QueryParamsProjects, user: User) -> ProjectList: +async def get_project_list(db: AsyncSession, edition: Edition, search_params: QueryParamsProjects, + user: User) -> ProjectList: """Returns a list of all projects from a certain edition""" - return ProjectList(projects=crud.get_projects_for_edition_page(db, edition, search_params, user)) + proj_page = await crud.get_projects_for_edition_page(db, edition, search_params, user) + return ProjectList(projects=proj_page) -def create_project(db: Session, edition: Edition, input_project: InputProject) -> Project: +async def create_project(db: AsyncSession, edition: Edition, input_project: InputProject) -> Project: """Create a new project""" - return crud.add_project(db, edition, input_project) + try: + # Fetch or create all partners + partners = await partners_logic.get_or_create_partners_by_name(db, input_project.partners, commit=False) + # Create the project + project = await crud.create_project(db, edition, input_project, partners, commit=False) -def delete_project(db: Session, project_id: int): - """Delete a project""" - crud.delete_project(db, project_id) + # Save the changes to the database + await db.commit() + # query the project to create the association tables + (await db.execute(select(Project).where(Project.project_id == project.project_id))) + return project + except Exception as ex: + # When an error occurs undo al database changes + await db.rollback() + raise ex -def patch_project(db: Session, project_id: int, input_project: InputProject): +async def patch_project(db: AsyncSession, project: Project, input_project: InputProject) -> Project: """Make changes to a project""" - crud.patch_project(db, project_id, input_project) + try: + # Fetch or create all partners + partners = await partners_logic.get_or_create_partners_by_name(db, input_project.partners, commit=False) + + await crud.patch_project(db, project, input_project, partners, commit=False) + + # Save the changes to the database + await db.commit() + + return project + except Exception as ex: + # When an error occurs undo al database changes + await db.rollback() + raise ex + + +async def delete_project(db: AsyncSession, project: Project): + """Delete a project""" + await crud.delete_project(db, project) -def get_conflicts(db: Session, edition: Edition) -> ConflictStudentList: +async def get_project_roles(db: AsyncSession, project: Project) -> ProjectRoleResponseList: + """Get project roles for a project""" + return ProjectRoleResponseList(project_roles=(await crud.get_project_roles_for_project(db, project))) + + +async def create_project_role(db: AsyncSession, project: Project, input_project_role: InputProjectRole) -> ProjectRole: + """Create a project role""" + if input_project_role.slots > 0: + return await crud.create_project_role(db, project, input_project_role) + raise NoStrictlyPositiveNumberOfSlots + + +async def patch_project_role(db: AsyncSession, project_role_id: int, input_project_role: InputProjectRole) \ + -> ProjectRole: + """Update a project role""" + if input_project_role.slots > 0: + return await crud.patch_project_role(db, project_role_id, input_project_role) + raise NoStrictlyPositiveNumberOfSlots + + +async def get_conflicts(db: AsyncSession, edition: Edition) -> ConflictStudentList: """Returns a list of all students together with the projects they are causing a conflict for""" - conflicts = crud.get_conflict_students(db, edition) - conflicts_model = [] - for student, projects in conflicts: - conflicts_model.append(ConflictStudent(student=student, projects=projects)) + return ConflictStudentList(conflict_students=(await crud.get_conflict_students(db, edition))) + - return ConflictStudentList(conflict_students=conflicts_model) +async def delete_project_role(db: AsyncSession, project_role_id: int) -> None: + """delete a project role""" + await crud.delete_project_role(db, project_role_id) diff --git a/backend/src/app/logic/projects_students.py b/backend/src/app/logic/projects_students.py index 12abaf612..ad89cda85 100644 --- a/backend/src/app/logic/projects_students.py +++ b/backend/src/app/logic/projects_students.py @@ -1,68 +1,38 @@ -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession -import src.app.logic.projects as logic_projects import src.database.crud.projects_students as crud -from src.app.exceptions.projects import StudentInConflictException, FailedToAddProjectRoleException -from src.app.schemas.projects import ConflictStudentList -from src.database.models import Project, ProjectRole, Student, Skill +from src.app.exceptions.crud import DuplicateInsertException +from src.app.schemas.projects import ( + InputArgumentation, ReturnProjectRoleSuggestion) +from src.database.models import ProjectRole, Student, User, ProjectRoleSuggestion -def remove_student_project(db: Session, project: Project, student_id: int): +async def remove_project_role_suggestion(db: AsyncSession, project_role: ProjectRole, student: Student): """Remove a student from a project""" - crud.remove_student_project(db, project, student_id) + await crud.remove_project_role_suggestion(db, project_role, student) -def add_student_project(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): +async def add_student_project( + db: AsyncSession, + project_role: ProjectRole, + student: Student, + drafter: User, + argumentation: InputArgumentation) -> ReturnProjectRoleSuggestion: """Add a student to a project""" - # check this project-skill combination does not exist yet - if db.query(ProjectRole).where(ProjectRole.skill_id == skill_id).where(ProjectRole.project == project) \ - .count() > 0: - raise FailedToAddProjectRoleException - # check that the student has the skill - student = db.query(Student).where(Student.student_id == student_id).one() - skill = db.query(Skill).where(Skill.skill_id == skill_id).one() - if skill not in student.skills: - raise FailedToAddProjectRoleException - # check that the student has not been confirmed in another project yet - if db.query(ProjectRole).where(ProjectRole.student == student).where(ProjectRole.definitive.is_(True)).count() > 0: - raise FailedToAddProjectRoleException - # check that the project requires the skill - project = db.query(Project).where(Project.project_id == project.project_id).one() - if skill not in project.skills: - raise FailedToAddProjectRoleException - - crud.add_student_project(db, project, student_id, skill_id, drafter_id) - - -def change_project_role(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): + pr_suggestion = await crud.get_optional_pr_suggestion_for_pr_by_student(db, project_role, student) + if pr_suggestion is None: + project_role_suggestion: ProjectRoleSuggestion = \ + await crud.create_pr_suggestion(db, project_role, student, drafter, argumentation) + return ReturnProjectRoleSuggestion(project_role_suggestion=project_role_suggestion) + raise DuplicateInsertException() + + +async def change_project_role_suggestion( + db: AsyncSession, + project_role: ProjectRole, + student: Student, + updater: User, + argumentation: InputArgumentation): """Change the role of the student in the project""" - # check this project-skill combination does not exist yet - if db.query(ProjectRole).where(ProjectRole.skill_id == skill_id).where(ProjectRole.project == project) \ - .count() > 0: - raise FailedToAddProjectRoleException - # check that the student has the skill - student = db.query(Student).where(Student.student_id == student_id).one() - skill = db.query(Skill).where(Skill.skill_id == skill_id).one() - if skill not in student.skills: - raise FailedToAddProjectRoleException - # check that the student has not been confirmed in another project yet - if db.query(ProjectRole).where(ProjectRole.student == student).where( - ProjectRole.definitive.is_(True)).count() > 0: - raise FailedToAddProjectRoleException - # check that the project requires the skill - project = db.query(Project).where(Project.project_id == project.project_id).one() - if skill not in project.skills: - raise FailedToAddProjectRoleException - - crud.change_project_role(db, project, student_id, skill_id, drafter_id) - - -def confirm_project_role(db: Session, project: Project, student_id: int): - """Definitively bind this student to the project""" - # check if there are any conflicts concerning this student - conflict_list: ConflictStudentList = logic_projects.get_conflicts(db, project.edition) - for conflict in conflict_list.conflict_students: - if conflict.student.student_id == student_id: - raise StudentInConflictException - - crud.confirm_project_role(db, project, student_id) + pr_suggestion = await crud.get_pr_suggestion_for_pr_by_student(db, project_role, student) + await crud.update_pr_suggestion(db, pr_suggestion, updater, argumentation) diff --git a/backend/src/app/logic/register.py b/backend/src/app/logic/register.py index 9a48ffa95..1bccbc007 100644 --- a/backend/src/app/logic/register.py +++ b/backend/src/app/logic/register.py @@ -1,27 +1,53 @@ +from uuid import UUID + import sqlalchemy.exc -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession +from src.app.exceptions.crud import DuplicateInsertException from src.app.exceptions.register import FailedToAddNewUserException from src.app.logic.security import get_password_hash -from src.app.schemas.register import NewUser +from src.app.schemas.oauth.github import GitHubProfile +from src.app.schemas.register import EmailRegister from src.database.crud.invites import get_invite_link_by_uuid, delete_invite_link -from src.database.crud.register import create_coach_request, create_user, create_auth_email +from src.database.crud.register import create_coach_request, create_user, create_auth_email, create_auth_github from src.database.models import Edition, InviteLink -def create_request(db: Session, new_user: NewUser, edition: Edition) -> None: - """Create a coach request. If something fails, the changes aren't committed""" - invite_link: InviteLink = get_invite_link_by_uuid(db, new_user.uuid) +async def create_request_email(db: AsyncSession, new_user: EmailRegister, edition: Edition): + """Create a coach request using email-password auth""" + invite_link: InviteLink = await get_invite_link_by_uuid(db, new_user.uuid) try: # Make all functions in here not commit anymore, # so we can roll back at the end if we have to - user = create_user(db, new_user.name, commit=False) - create_auth_email(db, user, get_password_hash(new_user.pw), new_user.email, commit=False) - create_coach_request(db, user, edition, commit=False) - delete_invite_link(db, invite_link, commit=False) + user = await create_user(db, new_user.name, commit=False) + await create_auth_email(db, user, get_password_hash(new_user.pw), new_user.email, commit=False) + await create_coach_request(db, user, edition, commit=False) + await delete_invite_link(db, invite_link, commit=False) + + await db.commit() + except sqlalchemy.exc.IntegrityError as exception: + await db.rollback() + raise DuplicateInsertException from exception + except sqlalchemy.exc.SQLAlchemyError as exception: + await db.rollback() + raise FailedToAddNewUserException from exception + + +async def create_request_github(db: AsyncSession, profile: GitHubProfile, uuid: UUID, edition: Edition): + """Create a coach request using GitHub auth""" + invite_link: InviteLink = await get_invite_link_by_uuid(db, uuid) + + try: + user = await create_user(db, profile.name, commit=False) + await create_auth_github(db, user, profile, commit=False) + await create_coach_request(db, user, edition, commit=False) + await delete_invite_link(db, invite_link, commit=False) - db.commit() + await db.commit() + except sqlalchemy.exc.IntegrityError as exception: + await db.rollback() + raise DuplicateInsertException from exception except sqlalchemy.exc.SQLAlchemyError as exception: - db.rollback() + await db.rollback() raise FailedToAddNewUserException from exception diff --git a/backend/src/app/logic/security.py b/backend/src/app/logic/security.py index 22fd39d60..3e3f26278 100644 --- a/backend/src/app/logic/security.py +++ b/backend/src/app/logic/security.py @@ -1,12 +1,14 @@ import enum from datetime import timedelta, datetime +import aiohttp from jose import jwt from passlib.context import CryptContext -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession import settings from src.app.exceptions.authentication import InvalidCredentialsException +from src.app.logic.oauth.github import get_user_by_github_code from src.database import models from src.database.crud.users import get_user_by_email from src.database.models import User @@ -54,11 +56,16 @@ def get_password_hash(password: str) -> str: return pwd_context.hash(password) -def authenticate_user(db: Session, email: str, password: str) -> models.User: +async def authenticate_user_email(db: AsyncSession, email: str, password: str) -> models.User: """Match an email/password combination to a User model""" - user = get_user_by_email(db, email) + user = await get_user_by_email(db, email) if user.email_auth.pw_hash is None or not verify_password(password, user.email_auth.pw_hash): raise InvalidCredentialsException() return user + + +async def authenticate_user_github(http_session: aiohttp.ClientSession, db: AsyncSession, code: str) -> models.User: + """Match a GitHub code to a User model""" + return await get_user_by_github_code(http_session, db, code) diff --git a/backend/src/app/logic/skills.py b/backend/src/app/logic/skills.py index 19a86d308..7440c6376 100644 --- a/backend/src/app/logic/skills.py +++ b/backend/src/app/logic/skills.py @@ -1,11 +1,11 @@ -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession import src.database.crud.skills as crud_skills from src.app.schemas.skills import SkillBase, SkillList from src.database.models import Skill -def get_skills(db: Session) -> SkillList: +async def get_skills(db: AsyncSession) -> SkillList: """Get a list of all the base skills that can be added to a student or project. Args: @@ -14,10 +14,11 @@ def get_skills(db: Session) -> SkillList: Returns: SkillList: an object with a list of all the skills. """ - return SkillList(skills=crud_skills.get_skills(db)) + skills = await crud_skills.get_skills(db) + return SkillList(skills=skills) -def create_skill(db: Session, skill: SkillBase) -> Skill: +async def create_skill(db: AsyncSession, skill: SkillBase) -> Skill: """Add a new skill into the database. Args: @@ -27,14 +28,14 @@ def create_skill(db: Session, skill: SkillBase) -> Skill: Returns: Skill: returns the new skill. """ - return crud_skills.create_skill(db, skill) + return await crud_skills.create_skill(db, skill) -def delete_skill(db: Session, skill_id: int): +async def delete_skill(db: AsyncSession, skill_id: int): """Delete an existing skill. Args: skill_id (int): the id of the skill. db (Session): connection with the database. """ - crud_skills.delete_skill(db, skill_id) + await crud_skills.delete_skill(db, skill_id) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index d506fbe8d..ecb22c9a7 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -1,15 +1,17 @@ -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm.exc import NoResultFound from src.app.schemas.students import NewDecision from src.database.crud.skills import get_skills_by_ids -from src.database.crud.students import (get_last_emails_of_students, get_student_by_id, - set_definitive_decision_on_student, - delete_student, get_students, get_emails, - create_email) +from src.database.crud.students import ( + get_last_emails_of_students, get_student_by_id, + set_definitive_decision_on_student, + delete_student, get_students, get_emails, + create_email +) from src.database.crud.suggestions import get_suggestions_of_student_by_type from src.database.enums import DecisionEnum -from src.database.models import Edition, Student, Skill, DecisionEmail +from src.database.models import Edition, Student, Skill, DecisionEmail, User from src.app.schemas.students import ( ReturnStudentList, ReturnStudent, CommonQueryParams, ReturnStudentMailList, Student as StudentModel, Suggestions as SuggestionsModel, @@ -17,84 +19,92 @@ ListReturnStudentMailList) -def definitive_decision_on_student(db: Session, student: Student, decision: NewDecision) -> None: +async def definitive_decision_on_student(db: AsyncSession, student: Student, decision: NewDecision) -> None: """Set a definitive decion on a student""" - set_definitive_decision_on_student(db, student, decision.decision) + await set_definitive_decision_on_student(db, student, decision.decision) -def remove_student(db: Session, student: Student) -> None: +async def remove_student(db: AsyncSession, student: Student) -> None: """delete a student""" - delete_student(db, student) - - -def get_students_search(db: Session, edition: Edition, commons: CommonQueryParams) -> ReturnStudentList: + await delete_student(db, student) + + +async def _get_student_with_suggestions(db: AsyncSession, student: Student) -> StudentModel: + nr_of_yes_suggestions = len(await get_suggestions_of_student_by_type( + db, student.student_id, DecisionEnum.YES)) + nr_of_no_suggestions = len(await get_suggestions_of_student_by_type( + db, student.student_id, DecisionEnum.NO)) + nr_of_maybe_suggestions = len(await get_suggestions_of_student_by_type( + db, student.student_id, DecisionEnum.MAYBE)) + suggestions = SuggestionsModel( + yes=nr_of_yes_suggestions, no=nr_of_no_suggestions, maybe=nr_of_maybe_suggestions) + return StudentModel(student_id=student.student_id, + first_name=student.first_name, + last_name=student.last_name, + preferred_name=student.preferred_name, + email_address=student.email_address, + phone_number=student.phone_number, + alumni=student.alumni, + finalDecision=student.decision, + wants_to_be_student_coach=student.wants_to_be_student_coach, + edition_id=student.edition_id, + skills=student.skills, + nr_of_suggestions=suggestions) + + +async def get_students_search(db: AsyncSession, edition: Edition, + commons: CommonQueryParams, user: User) -> ReturnStudentList: """return all students""" if commons.skill_ids: - skills: list[Skill] = get_skills_by_ids(db, commons.skill_ids) + skills: list[Skill] = await get_skills_by_ids(db, commons.skill_ids) if len(skills) != len(commons.skill_ids): return ReturnStudentList(students=[]) else: skills = [] - students_orm = get_students(db, edition, commons, skills) + students_orm = await get_students(db, edition, commons, user, skills) students: list[StudentModel] = [] for student in students_orm: - students.append(StudentModel( - student_id=student.student_id, - first_name=student.first_name, - last_name=student.last_name, - preferred_name=student.preferred_name, - email_address=student.email_address, - phone_number=student.phone_number, - alumni=student.alumni, - finalDecision=student.decision, - wants_to_be_student_coach=student.wants_to_be_student_coach, - edition_id=student.edition_id, - skills=student.skills)) - nr_of_yes_suggestions = len(get_suggestions_of_student_by_type( - db, student.student_id, DecisionEnum.YES)) - nr_of_no_suggestions = len(get_suggestions_of_student_by_type( - db, student.student_id, DecisionEnum.NO)) - nr_of_maybe_suggestions = len(get_suggestions_of_student_by_type( - db, student.student_id, DecisionEnum.MAYBE)) - students[-1].nr_of_suggestions = SuggestionsModel( - yes=nr_of_yes_suggestions, no=nr_of_no_suggestions, maybe=nr_of_maybe_suggestions) + student_model = await _get_student_with_suggestions(db, student) + students.append(student_model) return ReturnStudentList(students=students) -def get_student_return(student: Student, edition: Edition) -> ReturnStudent: + +async def get_student_return(db: AsyncSession, student: Student, edition: Edition) -> ReturnStudent: """return a student""" if student.edition == edition: - return ReturnStudent(student=student) + student_model = await _get_student_with_suggestions(db, student) + return ReturnStudent(student=student_model) raise NoResultFound -def get_emails_of_student(db: Session, edition: Edition, student: Student) -> ReturnStudentMailList: +async def get_emails_of_student(db: AsyncSession, edition: Edition, student: Student) -> ReturnStudentMailList: """returns all mails of a student""" if student.edition != edition: raise NoResultFound - emails: list[DecisionEmail] = get_emails(db, student) + emails: list[DecisionEmail] = await get_emails(db, student) return ReturnStudentMailList(emails=emails, student=student) -def make_new_email(db: Session, edition: Edition, new_email: NewEmail) -> ListReturnStudentMailList: +async def make_new_email(db: AsyncSession, edition: Edition, new_email: NewEmail) -> ListReturnStudentMailList: """make a new email""" student_emails: list[ReturnStudentMailList] = [] for student_id in new_email.students_id: - student: Student = get_student_by_id(db, student_id) + student: Student = await get_student_by_id(db, student_id) if student.edition == edition: - email: DecisionEmail = create_email(db, student, new_email.email_status) + email: DecisionEmail = await create_email(db, student, new_email.email_status) student_emails.append( ReturnStudentMailList(student=student, emails=[email]) ) return ListReturnStudentMailList(student_emails=student_emails) -def last_emails_of_students(db: Session, edition: Edition, - commons: EmailsSearchQueryParams) -> ListReturnStudentMailList: +async def last_emails_of_students(db: AsyncSession, edition: Edition, + commons: EmailsSearchQueryParams) -> ListReturnStudentMailList: """get last emails of students with search params""" - emails: list[DecisionEmail] = get_last_emails_of_students( + emails: list[DecisionEmail] = await get_last_emails_of_students( db, edition, commons) student_emails: list[ReturnStudentMailList] = [] for email in emails: diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py index d2b0a6771..f5f1423db 100644 --- a/backend/src/app/logic/suggestions.py +++ b/backend/src/app/logic/suggestions.py @@ -1,4 +1,4 @@ -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from src.app.schemas.suggestion import NewSuggestion from src.database.crud.suggestions import ( @@ -8,49 +8,50 @@ from src.app.exceptions.authentication import MissingPermissionsException -def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, - user: User, student_id: int | None) -> SuggestionResponse: +async def make_new_suggestion(db: AsyncSession, new_suggestion: NewSuggestion, + user: User, student_id: int | None) -> SuggestionResponse: """"Make a new suggestion""" - own_suggestion = get_own_suggestion(db, student_id, user.user_id) + own_suggestion = await get_own_suggestion(db, student_id, user.user_id) if own_suggestion is None: - suggestion_orm = create_suggestion( + suggestion_orm = await create_suggestion( db, user.user_id, student_id, new_suggestion.suggestion, new_suggestion.argumentation) else: - update_suggestion(db, own_suggestion, new_suggestion.suggestion, new_suggestion.argumentation) + await update_suggestion(db, own_suggestion, new_suggestion.suggestion, new_suggestion.argumentation) suggestion_orm = own_suggestion suggestion = suggestion_model_to_schema(suggestion_orm) return SuggestionResponse(suggestion=suggestion) -def all_suggestions_of_student(db: Session, student_id: int | None) -> SuggestionListResponse: +async def all_suggestions_of_student(db: AsyncSession, student_id: int | None) -> SuggestionListResponse: """Get all suggestions of a student""" - suggestions_orm = get_suggestions_of_student(db, student_id) + suggestions_orm = await get_suggestions_of_student(db, student_id) all_suggestions = [] for suggestion in suggestions_orm: all_suggestions.append(suggestion_model_to_schema(suggestion)) return SuggestionListResponse(suggestions=all_suggestions) -def remove_suggestion(db: Session, suggestion: Suggestion, user: User) -> None: +async def remove_suggestion(db: AsyncSession, suggestion: Suggestion, user: User) -> None: """ Delete a suggestion Admins can delete all suggestions, coaches only their own suggestions """ if user.admin or suggestion.coach == user: - delete_suggestion(db, suggestion) + await delete_suggestion(db, suggestion) else: raise MissingPermissionsException -def change_suggestion(db: Session, new_suggestion: NewSuggestion, suggestion: Suggestion, user: User) -> None: +async def change_suggestion(db: AsyncSession, new_suggestion: NewSuggestion, suggestion: Suggestion, + user: User) -> None: """ Update a suggestion Admins can update all suggestions, coaches only their own suggestions """ if user.admin or suggestion.coach == user: - update_suggestion( + await update_suggestion( db, suggestion, new_suggestion.suggestion, new_suggestion.argumentation) else: raise MissingPermissionsException diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index 52ded0361..b0e9bd488 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -1,13 +1,13 @@ -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession import src.database.crud.users as users_crud from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, user_model_to_schema, \ FilterParameters, UserRequest -from src.database.models import User +from src.database.models import User, Edition -def get_users_list( - db: Session, +async def get_users_list( + db: AsyncSession, params: FilterParameters ) -> UsersListResponse: """ @@ -15,46 +15,47 @@ def get_users_list( and wrap the result in a pydantic model """ - users_orm = users_crud.get_users_filtered_page(db, params) + users_orm = await users_crud.get_users_filtered_page(db, params) return UsersListResponse(users=[user_model_to_schema(user) for user in users_orm]) -def get_user_editions(db: Session, user: User) -> list[str]: +async def get_user_editions(db: AsyncSession, user: User) -> list[Edition]: """Get all names of the editions this user is coach in""" - return users_crud.get_user_edition_names(db, user) + return await users_crud.get_user_editions(db, user) -def edit_admin_status(db: Session, user_id: int, admin: AdminPatch): +async def edit_admin_status(db: AsyncSession, user_id: int, admin: AdminPatch): """ Edit the admin-status of a user """ - users_crud.edit_admin_status(db, user_id, admin.admin) + await users_crud.edit_admin_status(db, user_id, admin.admin) -def add_coach(db: Session, user_id: int, edition_name: str): +async def add_coach(db: AsyncSession, user_id: int, edition_name: str): """ Add user as coach for the given edition """ - users_crud.add_coach(db, user_id, edition_name) - users_crud.remove_request_if_exists(db, user_id, edition_name) + await users_crud.add_coach(db, user_id, edition_name) + await users_crud.remove_request_if_exists(db, user_id, edition_name) -def remove_coach(db: Session, user_id: int, edition_name: str): +async def remove_coach(db: AsyncSession, user_id: int, edition_name: str): """ Remove user as coach for the given edition """ - users_crud.remove_coach(db, user_id, edition_name) + await users_crud.remove_coach(db, user_id, edition_name) -def remove_coach_all_editions(db: Session, user_id: int): +async def remove_coach_all_editions(db: AsyncSession, user_id: int): """ Remove user as coach from all editions """ - users_crud.remove_coach_all_editions(db, user_id) + await users_crud.remove_coach_all_editions(db, user_id) -def get_request_list(db: Session, edition_name: str | None, user_name: str | None, page: int) -> UserRequestsResponse: +async def get_request_list(db: AsyncSession, edition_name: str | None, user_name: str | None, page: int) \ + -> UserRequestsResponse: """ Query the database for a list of all user requests and wrap the result in a pydantic model @@ -64,9 +65,9 @@ def get_request_list(db: Session, edition_name: str | None, user_name: str | Non user_name = "" if edition_name is None: - requests = users_crud.get_requests_page(db, page, user_name) + requests = await users_crud.get_requests_page(db, page, user_name) else: - requests = users_crud.get_requests_for_edition_page(db, edition_name, page, user_name) + requests = await users_crud.get_requests_for_edition_page(db, edition_name, page, user_name) requests_model = [] for request in requests: @@ -76,15 +77,15 @@ def get_request_list(db: Session, edition_name: str | None, user_name: str | Non return UserRequestsResponse(requests=requests_model) -def accept_request(db: Session, request_id: int): +async def accept_request(db: AsyncSession, request_id: int): """ Accept user request """ - users_crud.accept_request(db, request_id) + await users_crud.accept_request(db, request_id) -def reject_request(db: Session, request_id: int): +async def reject_request(db: AsyncSession, request_id: int): """ Reject user request """ - users_crud.reject_request(db, request_id) + await users_crud.reject_request(db, request_id) diff --git a/backend/src/app/logic/webhooks.py b/backend/src/app/logic/webhooks.py index 15f89db6f..bac477188 100644 --- a/backend/src/app/logic/webhooks.py +++ b/backend/src/app/logic/webhooks.py @@ -1,16 +1,19 @@ +from datetime import datetime from typing import cast import sqlalchemy.exc -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from settings import FormMapping from src.app.exceptions.webhooks import WebhookProcessException from src.app.schemas.webhooks import WebhookEvent, Question, Form, QuestionUpload, QuestionOption -from src.database.enums import QuestionEnum as QE -from src.database.models import Question as QuestionModel, QuestionAnswer, QuestionFileAnswer, Student, Edition +from src.database.crud.skills import get_skill_by_name +from src.database.enums import QuestionEnum as QE, EmailStatusEnum +from src.database.models import ( + Question as QuestionModel, QuestionAnswer, QuestionFileAnswer, Skill, Student, Edition, DecisionEmail) -def process_webhook(edition: Edition, data: WebhookEvent, database: Session): +async def process_webhook(edition: Edition, data: WebhookEvent, database: AsyncSession): """ Process webhook data @@ -20,8 +23,31 @@ def process_webhook(edition: Edition, data: WebhookEvent, database: Session): questions: list[Question] = form.fields extra_questions: list[Question] = [] - attributes: dict = {'edition': edition} + attributes: dict = { + 'edition': edition, + 'skills': [] + } + + await process_main_questions(questions, extra_questions, attributes, database) + + student: Student = Student(**attributes) + database.add(student) + email: DecisionEmail = DecisionEmail( + student=student, decision=EmailStatusEnum.APPLIED, date=datetime.now()) + + database.add(email) + + process_remaining_questions(student, extra_questions, database) + try: + await database.commit() + except sqlalchemy.exc.IntegrityError as error: + raise WebhookProcessException('Unique Check Failed') from error + + +async def process_main_questions(questions: list[Question], extra_questions: list[Question], + attributes: dict, database: AsyncSession): + """Process main questions""" for question in questions: match FormMapping(question.key): case FormMapping.FIRST_NAME: @@ -40,9 +66,22 @@ def process_webhook(edition: Edition, data: WebhookEvent, database: Session): if option.id == question.value: attributes['wants_to_be_student_coach'] = "yes" in option.text.lower() break # Only 2 options, Yes and No. + case FormMapping.ALUMNI: + if question.options is not None: + for option in question.options: + if option.id == question.value: + attributes['alumni'] = "yes" in option.text.lower() + case FormMapping.ROLES: + if question.options is not None: + answers = cast(list[str], question.value) + for value in answers: + options = cast(list[QuestionOption], question.options) + for option in options: + if option.id == value and option.text != "Other": + skill: Skill = await get_skill_by_name(database, option.text) + attributes["skills"].append(skill) case _: extra_questions.append(question) - # Check all attributes are included and not None needed = { 'first_name', @@ -51,26 +90,17 @@ def process_webhook(edition: Edition, data: WebhookEvent, database: Session): 'email_address', 'phone_number', 'wants_to_be_student_coach', - 'edition' + 'alumni', + 'edition', + 'skills' } diff = set(attributes.keys()).symmetric_difference(needed) if len(diff) != 0: raise WebhookProcessException(f'Missing questions for Attributes {diff}') - student: Student = Student(**attributes) - - database.add(student) - - process_remaining_questions(student, extra_questions, database) - - try: - database.commit() - except sqlalchemy.exc.IntegrityError as error: - raise WebhookProcessException('Unique Check Failed') from error - -def process_remaining_questions(student: Student, questions: list[Question], database: Session): +def process_remaining_questions(student: Student, questions: list[Question], database: AsyncSession): """Process all remaining questions""" for question in questions: diff --git a/backend/src/app/routers/editions/editions.py b/backend/src/app/routers/editions/editions.py index 0c5e4acc3..359fb8cc6 100644 --- a/backend/src/app/routers/editions/editions.py +++ b/backend/src/app/routers/editions/editions.py @@ -1,18 +1,23 @@ -from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session +from asyncio import Queue + +from fastapi import APIRouter, Depends, WebSocket +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status +from starlette.responses import Response +from websockets.exceptions import ConnectionClosedOK from src.app.logic import editions as logic_editions from src.app.routers.tags import Tags -from src.app.schemas.editions import EditionBase, Edition, EditionList +from src.app.schemas.editions import EditionBase, Edition, EditionList, EditEdition from src.database.database import get_session -from src.database.models import User +from src.database.models import User, Edition as EditionDB from .invites import invites_router from .projects import projects_router from .register import registration_router from .students import students_router from .webhooks import webhooks_router -from ...utils.dependencies import require_admin, require_auth, require_coach +from ...utils.dependencies import require_admin, require_auth, require_coach, require_coach_ws, get_edition +from ...utils.websockets import DataPublisher, get_publisher # Don't add the "Editions" tag here, because then it gets applied # to all child routes as well @@ -31,61 +36,76 @@ editions_router.include_router(router, prefix="/{edition_name}") -@editions_router.get("/", response_model=EditionList, tags=[Tags.EDITIONS]) -async def get_editions(db: Session = Depends(get_session), user: User = Depends(require_auth), page: int = 0): - """Get a paginated list of all editions. - Args: - db (Session, optional): connection with the database. Defaults to Depends(get_session). - user (User, optional): the current logged in user. Defaults to Depends(require_auth). - page (int): the page to return. - - Returns: - EditionList: an object with a list of all the editions. - """ +@editions_router.get("", response_model=EditionList, tags=[Tags.EDITIONS]) +async def get_editions(db: AsyncSession = Depends(get_session), user: User = Depends(require_auth), page: int = 0): + """Get a paginated list of all editions.""" if user.admin: - return logic_editions.get_editions_page(db, page) + return await logic_editions.get_editions_page(db, page) return EditionList(editions=user.editions) -@editions_router.get("/{edition_name}", response_model=Edition, tags=[Tags.EDITIONS], - dependencies=[Depends(require_coach)]) -async def get_edition_by_name(edition_name: str, db: Session = Depends(get_session)): - """Get a specific edition. - - Args: - edition_name (str): the name of the edition that you want to get. - db (Session, optional): connection with the database. Defaults to Depends(get_session). - user (User, optional): the current logged in user. Defaults to Depends(get_current_active_user). - - Returns: - Edition: an edition. - """ - return logic_editions.get_edition_by_name(db, edition_name) - - -@editions_router.post("/", status_code=status.HTTP_201_CREATED, response_model=Edition, tags=[Tags.EDITIONS], - dependencies=[Depends(require_admin)]) -async def post_edition(edition: EditionBase, db: Session = Depends(get_session)): - """ Create a new edition. - - Args: - db (Session, optional): connection with the database. Defaults to Depends(get_session). - - Returns: - Edition: the newly made edition object. +@editions_router.patch("/{edition_name}", response_class=Response, tags=[Tags.EDITIONS], + dependencies=[Depends(require_admin)], status_code=status.HTTP_204_NO_CONTENT) +async def patch_edition(edit_edition: EditEdition, edition: EditionDB = Depends(get_edition), + db: AsyncSession = Depends(get_session)): + """Change the readonly status of an edition + Note that this route is not behind "get_editable_edition", because otherwise you'd never be able + to change the status back to False """ - return logic_editions.create_edition(db, edition) - - -@editions_router.delete("/{edition_name}", status_code=status.HTTP_204_NO_CONTENT, tags=[Tags.EDITIONS], - dependencies=[Depends(require_admin)]) -async def delete_edition(edition_name: str, db: Session = Depends(get_session)): - """Delete an existing edition. - - Args: - edition_name (str): the name of the edition that needs to be deleted, if found. - db (Session, optional): connection with the database. Defaults to Depends(get_session). - + await logic_editions.patch_edition(db, edition, edit_edition.readonly) + + +@editions_router.get( + "/{edition_name}", + response_model=Edition, + tags=[Tags.EDITIONS], + dependencies=[Depends(require_coach)] +) +async def get_edition_by_name(edition_name: str, db: AsyncSession = Depends(get_session)): + """Get a specific edition.""" + return await logic_editions.get_edition_by_name(db, edition_name) + + +@editions_router.post( + "", + status_code=status.HTTP_201_CREATED, + response_model=Edition, + tags=[Tags.EDITIONS], + dependencies=[Depends(require_admin)] +) +async def post_edition(edition: EditionBase, db: AsyncSession = Depends(get_session)): + """ Create a new edition.""" + return await logic_editions.create_edition(db, edition) + + +@editions_router.delete( + "/{edition_name}", + status_code=status.HTTP_204_NO_CONTENT, response_class=Response, + tags=[Tags.EDITIONS], + dependencies=[Depends(require_admin)] +) +async def delete_edition(edition_name: str, db: AsyncSession = Depends(get_session)): + """Delete an existing edition.""" + await logic_editions.delete_edition(db, edition_name) + + +@editions_router.websocket('/{edition_name}/live') +async def feed( + websocket: WebSocket, + publisher: DataPublisher = Depends(get_publisher), + _: User = Depends(require_coach_ws) +): + """Handle websocket. + Events in the application are sent using this websocket """ - logic_editions.delete_edition(db, edition_name) + await websocket.accept() + queue: Queue = await publisher.subscribe() + try: + while True: + data: dict = await queue.get() + await websocket.send_json(data) + except ConnectionClosedOK: + pass + finally: + await publisher.unsubscribe(queue) diff --git a/backend/src/app/routers/editions/invites/invites.py b/backend/src/app/routers/editions/invites/invites.py index 4f2bc798d..ca61acee1 100644 --- a/backend/src/app/routers/editions/invites/invites.py +++ b/backend/src/app/routers/editions/invites/invites.py @@ -1,11 +1,11 @@ from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status from starlette.responses import Response from src.app.logic.invites import create_mailto_link, delete_invite_link, get_pending_invites_page from src.app.routers.tags import Tags -from src.app.utils.dependencies import get_edition, get_invite_link, require_admin, get_latest_edition +from src.app.utils.dependencies import get_edition, get_invite_link, require_admin, get_editable_edition from src.app.schemas.invites import InvitesLinkList, EmailAddress, NewInviteLink, InviteLink as InviteLinkModel from src.database.database import get_session from src.database.models import Edition, InviteLink as InviteLinkDB @@ -13,31 +13,31 @@ invites_router = APIRouter(prefix="/invites", tags=[Tags.INVITES]) -@invites_router.get("/", response_model=InvitesLinkList, dependencies=[Depends(require_admin)]) -async def get_invites(db: Session = Depends(get_session), edition: Edition = Depends(get_edition), page: int = 0): +@invites_router.get("", response_model=InvitesLinkList, dependencies=[Depends(require_admin)]) +async def get_invites(db: AsyncSession = Depends(get_session), edition: Edition = Depends(get_edition), page: int = 0): """ Get a list of all pending invitation links. """ - return get_pending_invites_page(db, edition, page) + return await get_pending_invites_page(db, edition, page) -@invites_router.post("/", status_code=status.HTTP_201_CREATED, response_model=NewInviteLink, +@invites_router.post("", status_code=status.HTTP_201_CREATED, response_model=NewInviteLink, dependencies=[Depends(require_admin)]) -async def create_invite(email: EmailAddress, db: Session = Depends(get_session), - edition: Edition = Depends(get_latest_edition)): +async def create_invite(email: EmailAddress, db: AsyncSession = Depends(get_session), + edition: Edition = Depends(get_editable_edition)): """ Create a new invitation link for the current edition. """ - return create_mailto_link(db, edition, email) + return await create_mailto_link(db, edition, email) @invites_router.delete("/{invite_uuid}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, dependencies=[Depends(require_admin), Depends(get_edition)]) -async def delete_invite(invite_link: InviteLinkDB = Depends(get_invite_link), db: Session = Depends(get_session)): +async def delete_invite(invite_link: InviteLinkDB = Depends(get_invite_link), db: AsyncSession = Depends(get_session)): """ Delete an existing invitation link manually so that it can't be used anymore. """ - delete_invite_link(db, invite_link) + await delete_invite_link(db, invite_link) @invites_router.get("/{invite_uuid}", response_model=InviteLinkModel, dependencies=[Depends(get_edition)]) diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index 0c55a326c..5da2e9fc5 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -1,73 +1,141 @@ from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status from starlette.responses import Response import src.app.logic.projects as logic from src.app.routers.tags import Tags -from src.app.utils.dependencies import get_edition, get_project, require_admin, require_coach, get_latest_edition -from src.app.schemas.projects import (ProjectList, Project, InputProject, - ConflictStudentList, QueryParamsProjects) +from src.app.schemas.projects import ( + InputProjectRole, + ProjectRole as ProjectRoleSchema, ProjectRoleResponseList) +from src.app.schemas.projects import ( + ProjectList, Project, InputProject, ConflictStudentList, QueryParamsProjects +) +from src.app.utils.dependencies import get_edition, get_project, require_admin, require_coach, get_editable_edition +from src.app.utils.websockets import live from src.database.database import get_session from src.database.models import Edition, Project as ProjectModel, User from .students import project_students_router projects_router = APIRouter(prefix="/projects", tags=[Tags.PROJECTS]) -projects_router.include_router(project_students_router, prefix="/{project_id}") +projects_router.include_router(project_students_router, prefix="/{project_id}/roles/{project_role_id}") -@projects_router.get("/", response_model=ProjectList) -async def get_projects(db: Session = Depends(get_session), edition: Edition = Depends(get_edition), - search_params: QueryParamsProjects = Depends(QueryParamsProjects), - user: User = Depends(require_coach)): - """ - Get a list of all projects. - """ - return logic.get_project_list(db, edition, search_params, user) +@projects_router.get("", response_model=ProjectList) +async def get_projects( + db: AsyncSession = Depends(get_session), + edition: Edition = Depends(get_edition), + search_params: QueryParamsProjects = Depends(QueryParamsProjects), + user: User = Depends(require_coach)): + """Get a list of all projects.""" + return await logic.get_project_list(db, edition, search_params, user) -@projects_router.post("/", status_code=status.HTTP_201_CREATED, response_model=Project, - dependencies=[Depends(require_admin)]) -async def create_project(input_project: InputProject, - db: Session = Depends(get_session), edition: Edition = Depends(get_latest_edition)): - """ - Create a new project - """ - return logic.create_project(db, edition, - input_project) +@projects_router.post( + "", + status_code=status.HTTP_201_CREATED, + response_model=Project, + dependencies=[Depends(require_admin)] +) +async def create_project( + input_project: InputProject, + db: AsyncSession = Depends(get_session), + edition: Edition = Depends(get_editable_edition)): + """Create a new project""" + return await logic.create_project(db, edition, + input_project) @projects_router.get("/conflicts", response_model=ConflictStudentList, dependencies=[Depends(require_coach)]) -async def get_conflicts(db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): +async def get_conflicts(db: AsyncSession = Depends(get_session), edition: Edition = Depends(get_edition)): """ Get a list of all projects with conflicts, and the users that are causing those conflicts. """ - return logic.get_conflicts(db, edition) + return await logic.get_conflicts(db, edition) -@projects_router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, - dependencies=[Depends(require_admin)]) -async def delete_project(project_id: int, db: Session = Depends(get_session)): - """ - Delete a specific project. - """ - return logic.delete_project(db, project_id) +@projects_router.delete( + "/{project_id}", + status_code=status.HTTP_204_NO_CONTENT, response_class=Response, + dependencies=[Depends(require_admin), Depends(live)] +) +async def delete_project(project: ProjectModel = Depends(get_project), db: AsyncSession = Depends(get_session)): + """Delete a specific project.""" + return await logic.delete_project(db, project) -@projects_router.get("/{project_id}", status_code=status.HTTP_200_OK, response_model=Project, - dependencies=[Depends(require_coach)]) +@projects_router.get( + "/{project_id}", + status_code=status.HTTP_200_OK, + response_model=Project, + dependencies=[Depends(require_coach)] +) async def get_project_route(project: ProjectModel = Depends(get_project)): - """ - Get information about a specific project. - """ + """Get information about a specific project.""" return project -@projects_router.patch("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, - dependencies=[Depends(require_admin), Depends(get_latest_edition)]) -async def patch_project(project_id: int, input_project: InputProject, db: Session = Depends(get_session)): +@projects_router.patch( + "/{project_id}", + status_code=status.HTTP_204_NO_CONTENT, response_class=Response, + dependencies=[Depends(require_admin), Depends(get_editable_edition), Depends(live)] +) +async def patch_project( + input_project: InputProject, + project: ProjectModel = Depends(get_project), + db: AsyncSession = Depends(get_session)): """ Update a project, changing some fields. """ - logic.patch_project(db, project_id, input_project) + await logic.patch_project(db, project, input_project) + + +@projects_router.get( + "/{project_id}/roles", + response_model=ProjectRoleResponseList, + dependencies=[Depends(require_coach), Depends(get_edition)] +) +async def get_project_roles(project: ProjectModel = Depends(get_project), db: AsyncSession = Depends(get_session)): + """List all project roles for a project""" + return await logic.get_project_roles(db, project) + + +@projects_router.post( + "/{project_id}/roles", + response_model=ProjectRoleSchema, + dependencies=[Depends(require_admin), Depends(get_editable_edition), Depends(live)], + status_code=status.HTTP_201_CREATED +) +async def post_project_role( + input_project_role: InputProjectRole, + project: ProjectModel = Depends(get_project), + db: AsyncSession = Depends(get_session)): + """Create a new project role""" + return await logic.create_project_role(db, project, input_project_role) + + +@projects_router.patch( + "/{project_id}/roles/{project_role_id}", + status_code=status.HTTP_200_OK, + response_model=ProjectRoleSchema, + dependencies=[Depends(require_admin), Depends(get_editable_edition), Depends(get_project), Depends(live)] +) +async def patch_project_role( + input_project_role: InputProjectRole, + project_role_id: int, + db: AsyncSession = Depends(get_session)): + """Create a new project role""" + return await logic.patch_project_role(db, project_role_id, input_project_role) + + +@projects_router.delete( + "/{project_id}/roles/{project_role_id}", + status_code=status.HTTP_204_NO_CONTENT, response_class=Response, + dependencies=[Depends(require_admin), Depends(get_project), Depends(live)] +) +async def delete_project_role( + project_role_id: int, + db: AsyncSession = Depends(get_session)): + """Delete a project role""" + await logic.delete_project_role(db, project_role_id) diff --git a/backend/src/app/routers/editions/projects/students/projects_students.py b/backend/src/app/routers/editions/projects/students/projects_students.py index 9839ed979..667439913 100644 --- a/backend/src/app/routers/editions/projects/students/projects_students.py +++ b/backend/src/app/routers/editions/projects/students/projects_students.py @@ -1,56 +1,69 @@ from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status from starlette.responses import Response import src.app.logic.projects_students as logic from src.app.routers.tags import Tags -from src.app.schemas.projects import InputStudentRole -from src.app.utils.dependencies import get_project, require_admin, require_coach, get_latest_edition +from src.app.schemas.projects import InputArgumentation, ReturnProjectRoleSuggestion +from src.app.utils.dependencies import ( + require_coach, get_editable_edition, get_student, + get_project_role, get_edition +) +from src.app.utils.websockets import live from src.database.database import get_session -from src.database.models import Project, User +from src.database.models import User, Student, ProjectRole project_students_router = APIRouter(prefix="/students", tags=[Tags.PROJECTS, Tags.STUDENTS]) -@project_students_router.delete("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, - dependencies=[Depends(require_coach)]) -async def remove_student_from_project(student_id: int, db: Session = Depends(get_session), - project: Project = Depends(get_project)): +@project_students_router.delete( + "/{student_id}", + status_code=status.HTTP_204_NO_CONTENT, response_class=Response, + dependencies=[Depends(require_coach), Depends(get_edition), Depends(live)] +) +async def remove_student_from_project( + student: Student = Depends(get_student), + db: AsyncSession = Depends(get_session), + project_role: ProjectRole = Depends(get_project_role)): """ Remove a student from a project. """ - logic.remove_student_project(db, project, student_id) + await logic.remove_project_role_suggestion(db, project_role, student) -@project_students_router.patch("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, - dependencies=[Depends(get_latest_edition)]) -async def change_project_role(student_id: int, input_sr: InputStudentRole, db: Session = Depends(get_session), - project: Project = Depends(get_project), user: User = Depends(require_coach)): +@project_students_router.patch( + "/{student_id}", + status_code=status.HTTP_204_NO_CONTENT, response_class=Response, + dependencies=[Depends(get_editable_edition), Depends(live)] +) +async def change_project_role( + argumentation: InputArgumentation, + student: Student = Depends(get_student), + db: AsyncSession = Depends(get_session), + project_role: ProjectRole = Depends(get_project_role), + user: User = Depends(require_coach)): """ Change the role a student is drafted for in a project. """ - logic.change_project_role(db, project, student_id, input_sr.skill_id, user.user_id) + await logic.change_project_role_suggestion(db, project_role, student, user, argumentation) -@project_students_router.post("/{student_id}", status_code=status.HTTP_201_CREATED, response_class=Response, - dependencies=[Depends(get_latest_edition)]) -async def add_student_to_project(student_id: int, input_sr: InputStudentRole, db: Session = Depends(get_session), - project: Project = Depends(get_project), user: User = Depends(require_coach)): +@project_students_router.post( + "/{student_id}", + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(get_editable_edition), Depends(live)], + response_model=ReturnProjectRoleSuggestion +) +async def add_student_to_project( + argumentation: InputArgumentation, + student: Student = Depends(get_student), + db: AsyncSession = Depends(get_session), + project_role: ProjectRole = Depends(get_project_role), + user: User = Depends(require_coach)): """ Add a student to a project. This is not a definitive decision, but represents a coach drafting the student. """ - logic.add_student_project(db, project, student_id, input_sr.skill_id, user.user_id) - - -@project_students_router.post("/{student_id}/confirm", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, - dependencies=[Depends(require_admin), Depends(get_latest_edition)]) -async def confirm_project_role(student_id: int, db: Session = Depends(get_session), - project: Project = Depends(get_project)): - """ - Definitively add a student to a project (confirm its role). - This can only be performed by an admin. - """ - logic.confirm_project_role(db, project, student_id) + return await logic.add_student_project(db, project_role, student, user, argumentation) diff --git a/backend/src/app/routers/editions/register/register.py b/backend/src/app/routers/editions/register/register.py index e008c13b6..24eab4f0e 100644 --- a/backend/src/app/routers/editions/register/register.py +++ b/backend/src/app/routers/editions/register/register.py @@ -1,11 +1,13 @@ +from aiohttp import ClientSession from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status -from src.app.logic.register import create_request +from src.app.logic.oauth.github import get_github_access_token, get_github_profile +from src.app.logic.register import create_request_email, create_request_github from src.app.routers.tags import Tags -from src.app.schemas.register import NewUser -from src.app.utils.dependencies import get_latest_edition +from src.app.schemas.register import EmailRegister, GitHubRegister +from src.app.utils.dependencies import get_editable_edition, get_http_session from src.database.database import get_session from src.database.models import Edition @@ -13,9 +15,19 @@ @registration_router.post("/email", status_code=status.HTTP_201_CREATED) -async def register_email(user: NewUser, db: Session = Depends(get_session), - edition: Edition = Depends(get_latest_edition)): +async def register_email(register_data: EmailRegister, db: AsyncSession = Depends(get_session), + edition: Edition = Depends(get_editable_edition)): """ Register a new account using the email/password format. """ - create_request(db, user, edition) + await create_request_email(db, register_data, edition) + + +@registration_router.post("/github", status_code=status.HTTP_201_CREATED) +async def register_github(register_data: GitHubRegister, db: AsyncSession = Depends(get_session), + http_session: ClientSession = Depends(get_http_session), + edition: Edition = Depends(get_editable_edition)): + """Register a new account using GitHub OAuth.""" + access_token_data = await get_github_access_token(http_session, register_data.code) + user_email = await get_github_profile(http_session, access_token_data.access_token) + await create_request_github(db, user_email, register_data.uuid, edition) diff --git a/backend/src/app/routers/editions/students/answers/__init__.py b/backend/src/app/routers/editions/students/answers/__init__.py new file mode 100644 index 000000000..4549d9bc2 --- /dev/null +++ b/backend/src/app/routers/editions/students/answers/__init__.py @@ -0,0 +1 @@ +from .answers import students_answers_router diff --git a/backend/src/app/routers/editions/students/answers/answers.py b/backend/src/app/routers/editions/students/answers/answers.py new file mode 100644 index 000000000..9ecd1ec34 --- /dev/null +++ b/backend/src/app/routers/editions/students/answers/answers.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter, Depends + +from starlette import status +from src.app.logic.answers import gives_question_and_answers +from src.app.routers.tags import Tags +from src.app.utils.dependencies import get_student, require_coach +from src.app.schemas.answers import Questions +from src.database.models import Student + +students_answers_router = APIRouter( + prefix="/answers", tags=[Tags.STUDENTS]) + + +@students_answers_router.get("", status_code=status.HTTP_200_OK, response_model=Questions, + dependencies=[Depends(require_coach)]) +async def get_answers(student: Student = Depends(get_student)): + """give answers of a student""" + return await gives_question_and_answers(student=student) diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index 076b43222..090ec03a7 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -1,88 +1,123 @@ from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session - +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status -from src.app.routers.tags import Tags -from src.app.utils.dependencies import get_student, get_edition, require_admin, require_auth +from starlette.responses import Response + from src.app.logic.students import ( definitive_decision_on_student, remove_student, get_student_return, get_students_search, get_emails_of_student, make_new_email, - last_emails_of_students) -from src.app.schemas.students import (NewDecision, CommonQueryParams, ReturnStudent, ReturnStudentList, - ReturnStudentMailList, NewEmail, EmailsSearchQueryParams, - ListReturnStudentMailList) + last_emails_of_students +) +from src.app.routers.tags import Tags +from src.app.schemas.students import ( + NewDecision, CommonQueryParams, ReturnStudent, ReturnStudentList, + ReturnStudentMailList, NewEmail, EmailsSearchQueryParams, + ListReturnStudentMailList +) +from src.app.utils.dependencies import get_editable_edition, get_student, get_edition, require_admin, require_coach +from src.app.utils.websockets import live from src.database.database import get_session -from src.database.models import Student, Edition +from src.database.models import Student, Edition, User from .suggestions import students_suggestions_router +from .answers import students_answers_router students_router = APIRouter(prefix="/students", tags=[Tags.STUDENTS]) students_router.include_router( students_suggestions_router, prefix="/{student_id}") +students_router.include_router( + students_answers_router, prefix="/{student_id}") -@students_router.get("/", dependencies=[Depends(require_auth)], response_model=ReturnStudentList) -async def get_students(db: Session = Depends(get_session), +@students_router.get("", response_model=ReturnStudentList) +async def get_students(db: AsyncSession = Depends(get_session), commons: CommonQueryParams = Depends(CommonQueryParams), - edition: Edition = Depends(get_edition)): + edition: Edition = Depends(get_edition), user: User = Depends(require_coach)): """ Get a list of all students. """ - return get_students_search(db, edition, commons) - - -@students_router.post("/emails", dependencies=[Depends(require_admin)], - status_code=status.HTTP_201_CREATED, response_model=ListReturnStudentMailList) -async def send_emails(new_email: NewEmail, db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): + return await get_students_search(db, edition, commons, user) + + +@students_router.post( + "/emails", + dependencies=[Depends(require_admin)], + status_code=status.HTTP_201_CREATED, + response_model=ListReturnStudentMailList +) +async def send_emails( + new_email: NewEmail, + db: AsyncSession = Depends(get_session), + edition: Edition = Depends(get_editable_edition)): """ Send a email to a list of students. """ - return make_new_email(db, edition, new_email) - - -@students_router.get("/emails", dependencies=[Depends(require_admin)], - response_model=ListReturnStudentMailList) -async def get_emails(db: Session = Depends(get_session), edition: Edition = Depends(get_edition), - commons: EmailsSearchQueryParams = Depends(EmailsSearchQueryParams)): + return await make_new_email(db, edition, new_email) + + +@students_router.get( + "/emails", + dependencies=[Depends(require_admin)], + response_model=ListReturnStudentMailList +) +async def get_emails( + db: AsyncSession = Depends(get_session), + edition: Edition = Depends(get_edition), + commons: EmailsSearchQueryParams = Depends(EmailsSearchQueryParams)): """ Get last emails of students """ - return last_emails_of_students(db, edition, commons) + return await last_emails_of_students(db, edition, commons) -@students_router.delete("/{student_id}", dependencies=[Depends(require_admin)], status_code=status.HTTP_204_NO_CONTENT) -async def delete_student(student: Student = Depends(get_student), db: Session = Depends(get_session)): +@students_router.delete( + "/{student_id}", + dependencies=[Depends(require_admin), Depends(live)], + status_code=status.HTTP_204_NO_CONTENT, response_class=Response, +) +async def delete_student(student: Student = Depends(get_student), db: AsyncSession = Depends(get_session)): """ Delete all information stored about a specific student. """ - remove_student(db, student) + await remove_student(db, student) -@students_router.get("/{student_id}", dependencies=[Depends(require_auth)], response_model=ReturnStudent) -async def get_student_by_id(edition: Edition = Depends(get_edition), student: Student = Depends(get_student)): +@students_router.get("/{student_id}", dependencies=[Depends(require_coach)], response_model=ReturnStudent) +async def get_student_by_id(edition: Edition = Depends(get_edition), student: Student = Depends(get_student), + db: AsyncSession = Depends(get_session)): """ Get information about a specific student. """ - return get_student_return(student, edition) - - -@students_router.put("/{student_id}/decision", dependencies=[Depends(require_admin)], - status_code=status.HTTP_204_NO_CONTENT) -async def make_decision(decision: NewDecision, student: Student = Depends(get_student), - db: Session = Depends(get_session)): + return await get_student_return(db, student, edition) + + +@students_router.put( + "/{student_id}/decision", + dependencies=[Depends(require_admin), Depends(live), Depends(get_editable_edition)], + status_code=status.HTTP_204_NO_CONTENT +) +async def make_decision( + decision: NewDecision, + student: Student = Depends(get_student), + db: AsyncSession = Depends(get_session)): """ Make a finalized Yes/Maybe/No decision about a student. This action can only be performed by an admin. """ - definitive_decision_on_student(db, student, decision) - - -@students_router.get("/{student_id}/emails", dependencies=[Depends(require_admin)], - response_model=ReturnStudentMailList) -async def get_student_email_history(edition: Edition = Depends(get_edition), student: Student = Depends(get_student), - db: Session = Depends(get_session)): + await definitive_decision_on_student(db, student, decision) + + +@students_router.get( + "/{student_id}/emails", + dependencies=[Depends(require_admin)], + response_model=ReturnStudentMailList +) +async def get_student_email_history( + edition: Edition = Depends(get_edition), + student: Student = Depends(get_student), + db: AsyncSession = Depends(get_session)): """ Get the history of all Yes/Maybe/No emails that have been sent to a specific student so far. """ - return get_emails_of_student(db, edition, student) + return await get_emails_of_student(db, edition, student) diff --git a/backend/src/app/routers/editions/students/suggestions/suggestions.py b/backend/src/app/routers/editions/students/suggestions/suggestions.py index 7fa06def7..1df110f5c 100644 --- a/backend/src/app/routers/editions/students/suggestions/suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/suggestions.py @@ -1,9 +1,12 @@ from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status +from starlette.responses import Response + from src.app.routers.tags import Tags -from src.app.utils.dependencies import require_auth, get_student, get_suggestion +from src.app.utils.dependencies import get_editable_edition, get_student, get_suggestion, require_coach +from src.app.utils.websockets import live from src.database.database import get_session from src.database.models import Student, User, Suggestion from src.app.logic.suggestions import (make_new_suggestion, all_suggestions_of_student, @@ -15,41 +18,53 @@ prefix="/suggestions", tags=[Tags.STUDENTS]) -@students_suggestions_router.post("/", status_code=status.HTTP_201_CREATED, response_model=SuggestionResponse) +@students_suggestions_router.post( + "", + status_code=status.HTTP_201_CREATED, + response_model=SuggestionResponse, + dependencies=[Depends(live), Depends(get_editable_edition)] +) async def create_suggestion(new_suggestion: NewSuggestion, student: Student = Depends(get_student), - db: Session = Depends(get_session), user: User = Depends(require_auth)): + db: AsyncSession = Depends(get_session), user: User = Depends(require_coach)): """ Make a suggestion about a student. In case you've already made a suggestion previously, this replaces the existing suggestion. This simplifies the process in frontend, so we can just send a new request without making an edit interface. """ - return make_new_suggestion(db, new_suggestion, user, student.student_id) + return await make_new_suggestion(db, new_suggestion, user, student.student_id) -@students_suggestions_router.delete("/{suggestion_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_suggestion(db: Session = Depends(get_session), user: User = Depends(require_auth), +@students_suggestions_router.delete( + "/{suggestion_id}", + status_code=status.HTTP_204_NO_CONTENT, response_class=Response, + dependencies=[Depends(live)] +) +async def delete_suggestion(db: AsyncSession = Depends(get_session), user: User = Depends(require_coach), suggestion: Suggestion = Depends(get_suggestion)): """ Delete a suggestion you made about a student. """ - remove_suggestion(db, suggestion, user) + await remove_suggestion(db, suggestion, user) -@students_suggestions_router.put("/{suggestion_id}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(get_student)]) -async def edit_suggestion(new_suggestion: NewSuggestion, db: Session = Depends(get_session), - user: User = Depends(require_auth), suggestion: Suggestion = Depends(get_suggestion)): +@students_suggestions_router.put( + "/{suggestion_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(get_student), Depends(live), Depends(get_editable_edition)] +) +async def edit_suggestion(new_suggestion: NewSuggestion, db: AsyncSession = Depends(get_session), + user: User = Depends(require_coach), suggestion: Suggestion = Depends(get_suggestion)): """ Edit a suggestion you made about a student. """ - change_suggestion(db, new_suggestion, suggestion, user) + await change_suggestion(db, new_suggestion, suggestion, user) -@students_suggestions_router.get("/", dependencies=[Depends(require_auth)], +@students_suggestions_router.get("", dependencies=[Depends(require_coach)], status_code=status.HTTP_200_OK, response_model=SuggestionListResponse) -async def get_suggestions(student: Student = Depends(get_student), db: Session = Depends(get_session)): +async def get_suggestions(student: Student = Depends(get_student), db: AsyncSession = Depends(get_session)): """ Get all suggestions of a student. """ - return all_suggestions_of_student(db, student.student_id) + return await all_suggestions_of_student(db, student.student_id) diff --git a/backend/src/app/routers/editions/webhooks/webhooks.py b/backend/src/app/routers/editions/webhooks/webhooks.py index 9a687aa7f..139818aef 100644 --- a/backend/src/app/routers/editions/webhooks/webhooks.py +++ b/backend/src/app/routers/editions/webhooks/webhooks.py @@ -1,11 +1,11 @@ from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status from src.app.logic.webhooks import process_webhook from src.app.routers.tags import Tags from src.app.schemas.webhooks import WebhookEvent, WebhookUrlResponse -from src.app.utils.dependencies import get_edition, require_admin, get_latest_edition +from src.app.utils.dependencies import get_edition, require_admin, get_editable_edition from src.database.crud.webhooks import get_webhook, create_webhook from src.database.database import get_session from src.database.models import Edition @@ -13,23 +13,24 @@ webhooks_router = APIRouter(prefix="/webhooks", tags=[Tags.WEBHOOKS]) -def valid_uuid(uuid: str, database: Session = Depends(get_session)): +async def valid_uuid(uuid: str, database: AsyncSession = Depends(get_session)): """Verify if uuid is a valid uuid""" - get_webhook(database, uuid) + await get_webhook(database, uuid) -@webhooks_router.post("/", response_model=WebhookUrlResponse, status_code=status.HTTP_201_CREATED, +@webhooks_router.post("", response_model=WebhookUrlResponse, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_admin)]) -def new(edition: Edition = Depends(get_latest_edition), database: Session = Depends(get_session)): +async def new(edition: Edition = Depends(get_editable_edition), database: AsyncSession = Depends(get_session)): """Create a new webhook for an edition""" - return create_webhook(database, edition) + return await create_webhook(database, edition) @webhooks_router.post("/{uuid}", dependencies=[Depends(valid_uuid)], status_code=status.HTTP_201_CREATED) -def webhook(data: WebhookEvent, edition: Edition = Depends(get_edition), database: Session = Depends(get_session)): +async def webhook(data: WebhookEvent, edition: Edition = Depends(get_edition), + database: AsyncSession = Depends(get_session)): """Receive a webhook event, This is triggered by Tally""" try: - process_webhook(edition, data, database) + await process_webhook(edition, data, database) except Exception as exception: # When processing fails, write the webhook data to a file to make sure it is not lost. with open(f'failed-webhook-{data.event_id}.json', 'w', encoding='utf-8') as file: diff --git a/backend/src/app/routers/login/login.py b/backend/src/app/routers/login/login.py index 093f5cd13..d4b33ee78 100644 --- a/backend/src/app/routers/login/login.py +++ b/backend/src/app/routers/login/login.py @@ -1,27 +1,29 @@ +import aiohttp import sqlalchemy.exc -from fastapi import APIRouter +from fastapi import APIRouter, Form from fastapi import Depends from fastapi.security import OAuth2PasswordRequestForm -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from src.app.exceptions.authentication import InvalidCredentialsException -from src.app.logic.security import authenticate_user, create_tokens +from src.app.logic.security import authenticate_user_email, create_tokens, authenticate_user_github from src.app.logic.users import get_user_editions from src.app.routers.tags import Tags +from src.app.schemas.editions import Edition from src.app.schemas.login import Token from src.app.schemas.users import user_model_to_schema -from src.app.utils.dependencies import get_user_from_refresh_token +from src.app.utils.dependencies import get_user_from_refresh_token, get_http_session from src.database.database import get_session from src.database.models import User login_router = APIRouter(prefix="/login", tags=[Tags.LOGIN]) -@login_router.post("/token", response_model=Token) -async def login_for_access_token(db: Session = Depends(get_session), form_data: OAuth2PasswordRequestForm = Depends()): +@login_router.post("/token/email", response_model=Token) +async def login_email(db: AsyncSession = Depends(get_session), form_data: OAuth2PasswordRequestForm = Depends()): """Called when logging in, generates an access token to use in other functions""" try: - user = authenticate_user(db, form_data.username, form_data.password) + user = await authenticate_user_email(db, form_data.username, form_data.password) except sqlalchemy.exc.NoResultFound as not_found: # Don't use our own error handler here because this should # be a 401 instead of a 404 @@ -30,8 +32,21 @@ async def login_for_access_token(db: Session = Depends(get_session), form_data: return await generate_token_response_for_user(db, user) +@login_router.post("/token/github") +async def login_github(http_session: aiohttp.ClientSession = Depends(get_http_session), + db: AsyncSession = Depends(get_session), code: str = Form(...)): + """Called when logging in through GitHub, generates an access token""" + try: + user = await authenticate_user_github(http_session, db, code) + except sqlalchemy.exc.NoResultFound as not_found: + raise InvalidCredentialsException() from not_found + + return await generate_token_response_for_user(db, user) + + @login_router.post("/refresh", response_model=Token) -async def refresh_access_token(db: Session = Depends(get_session), user: User = Depends(get_user_from_refresh_token)): +async def refresh_access_token(db: AsyncSession = Depends(get_session), + user: User = Depends(get_user_from_refresh_token)): """ Return a new access & refresh token using on the old refresh token @@ -40,14 +55,15 @@ async def refresh_access_token(db: Session = Depends(get_session), user: User = return await generate_token_response_for_user(db, user) -async def generate_token_response_for_user(db: Session, user: User) -> Token: +async def generate_token_response_for_user(db: AsyncSession, user: User) -> Token: """ Generate new tokens for a user and put them in the Token response schema. """ access_token, refresh_token = create_tokens(user) user_data: dict = user_model_to_schema(user).__dict__ - user_data["editions"] = get_user_editions(db, user) + editions = await get_user_editions(db, user) + user_data["editions"] = list(map(Edition.from_orm, editions)) return Token( access_token=access_token, diff --git a/backend/src/app/routers/skills/skills.py b/backend/src/app/routers/skills/skills.py index 2da8892b5..fb7d39660 100644 --- a/backend/src/app/routers/skills/skills.py +++ b/backend/src/app/routers/skills/skills.py @@ -1,51 +1,32 @@ from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status +from starlette.responses import Response from src.app.logic import skills as logic_skills from src.app.routers.tags import Tags from src.app.schemas.skills import SkillBase, Skill, SkillList -from src.app.utils.dependencies import require_auth +from src.app.utils.dependencies import require_admin, require_auth from src.database.database import get_session skills_router = APIRouter(prefix="/skills", tags=[Tags.SKILLS]) -@skills_router.get("/", response_model=SkillList, tags=[Tags.SKILLS], dependencies=[Depends(require_auth)]) -async def get_skills(db: Session = Depends(get_session)): - """Get a list of all the base skills that can be added to a student or project. +@skills_router.get("", response_model=SkillList, tags=[Tags.SKILLS], dependencies=[Depends(require_auth)]) +async def get_skills(db: AsyncSession = Depends(get_session)): + """Get a list of all the base skills that can be added to a student or project.""" + return await logic_skills.get_skills(db) - Args: - db (Session, optional): connection with the database. Defaults to Depends(get_session). - Returns: - SkillList: an object with a list of all the skills. - """ - return logic_skills.get_skills(db) +@skills_router.post("", status_code=status.HTTP_201_CREATED, response_model=Skill, tags=[Tags.SKILLS], + dependencies=[Depends(require_admin)]) +async def create_skill(skill: SkillBase, db: AsyncSession = Depends(get_session)): + """Add a new skill into the database.""" + return await logic_skills.create_skill(db, skill) -@skills_router.post("/", status_code=status.HTTP_201_CREATED, response_model=Skill, tags=[Tags.SKILLS], - dependencies=[Depends(require_auth)]) -async def create_skill(skill: SkillBase, db: Session = Depends(get_session)): - """Add a new skill into the database. - - Args: - skill (SkillBase): has all the fields needed to add a skill. - db (Session, optional): connection with the database. Defaults to Depends(get_session). - - Returns: - Skill: returns the new skill. - """ - return logic_skills.create_skill(db, skill) - - -@skills_router.delete("/{skill_id}", status_code=status.HTTP_204_NO_CONTENT, tags=[Tags.SKILLS], - dependencies=[Depends(require_auth)]) -async def delete_skill(skill_id: int, db: Session = Depends(get_session)): - """Delete an existing skill. - - Args: - skill_id (int): the id of the skill. - db (Session, optional): connection with the database. Defaults to Depends(get_session). - """ - logic_skills.delete_skill(db, skill_id) +@skills_router.delete("/{skill_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, + tags=[Tags.SKILLS], dependencies=[Depends(require_admin)]) +async def delete_skill(skill_id: int, db: AsyncSession = Depends(get_session)): + """Delete an existing skill.""" + await logic_skills.delete_skill(db, skill_id) diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index 04452dc5d..5f815750a 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -1,9 +1,11 @@ from fastapi import APIRouter, Query, Depends -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status +from starlette.responses import Response import src.app.logic.users as logic from src.app.routers.tags import Tags +from src.app.schemas.editions import Edition from src.app.schemas.login import UserData from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, user_model_to_schema, \ FilterParameters @@ -14,61 +16,62 @@ users_router = APIRouter(prefix="/users", tags=[Tags.USERS]) -@users_router.get("/", response_model=UsersListResponse, dependencies=[Depends(require_admin)]) +@users_router.get("", response_model=UsersListResponse, dependencies=[Depends(require_admin)]) async def get_users( params: FilterParameters = Depends(), - db: Session = Depends(get_session)): + db: AsyncSession = Depends(get_session)): """ Get users When the admin parameter is True, the edition and exclude_edition parameter will have no effect. Since admins have access to all editions. """ - return logic.get_users_list(db, params) + return await logic.get_users_list(db, params) @users_router.get("/current", response_model=UserData) -async def get_current_user(db: Session = Depends(get_session), user: UserDB = Depends(get_user_from_access_token)): +async def get_current_user(db: AsyncSession = Depends(get_session), user: UserDB = Depends(get_user_from_access_token)): """Get a user based on their authorization credentials""" user_data = user_model_to_schema(user).__dict__ - user_data["editions"] = logic.get_user_editions(db, user) - + editions = await logic.get_user_editions(db, user) + user_data["editions"] = list(map(Edition.from_orm, editions)) return user_data -@users_router.patch("/{user_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_admin)]) -async def patch_admin_status(user_id: int, admin: AdminPatch, db: Session = Depends(get_session)): +@users_router.patch("/{user_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, + dependencies=[Depends(require_admin)]) +async def patch_admin_status(user_id: int, admin: AdminPatch, db: AsyncSession = Depends(get_session)): """ Set admin-status of user """ - logic.edit_admin_status(db, user_id, admin) + await logic.edit_admin_status(db, user_id, admin) @users_router.post("/{user_id}/editions/{edition_name}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(require_admin)]) -async def add_to_edition(user_id: int, edition_name: str, db: Session = Depends(get_session)): + response_class=Response, dependencies=[Depends(require_admin)]) +async def add_to_edition(user_id: int, edition_name: str, db: AsyncSession = Depends(get_session)): """ Add user as coach of the given edition """ - logic.add_coach(db, user_id, edition_name) + await logic.add_coach(db, user_id, edition_name) @users_router.delete("/{user_id}/editions/{edition_name}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(require_admin)]) -async def remove_from_edition(user_id: int, edition_name: str, db: Session = Depends(get_session)): + response_class=Response, dependencies=[Depends(require_admin)]) +async def remove_from_edition(user_id: int, edition_name: str, db: AsyncSession = Depends(get_session)): """ Remove user as coach of the given edition """ - logic.remove_coach(db, user_id, edition_name) + await logic.remove_coach(db, user_id, edition_name) -@users_router.delete("/{user_id}/editions", status_code=status.HTTP_204_NO_CONTENT, +@users_router.delete("/{user_id}/editions", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, dependencies=[Depends(require_admin)]) -async def remove_from_all_editions(user_id: int, db: Session = Depends(get_session)): +async def remove_from_all_editions(user_id: int, db: AsyncSession = Depends(get_session)): """ Remove user as coach from all editions """ - logic.remove_coach_all_editions(db, user_id) + await logic.remove_coach_all_editions(db, user_id) @users_router.get("/requests", response_model=UserRequestsResponse, dependencies=[Depends(require_admin)]) @@ -76,26 +79,26 @@ async def get_requests( edition: str | None = Query(None), user: str | None = Query(None), page: int = 0, - db: Session = Depends(get_session)): + db: AsyncSession = Depends(get_session)): """ Get pending userrequests """ - return logic.get_request_list(db, edition, user, page) + return await logic.get_request_list(db, edition, user, page) -@users_router.post("/requests/{request_id}/accept", status_code=status.HTTP_204_NO_CONTENT, +@users_router.post("/requests/{request_id}/accept", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, dependencies=[Depends(require_admin)]) -async def accept_request(request_id: int, db: Session = Depends(get_session)): +async def accept_request(request_id: int, db: AsyncSession = Depends(get_session)): """ Accept a coach request """ - logic.accept_request(db, request_id) + await logic.accept_request(db, request_id) -@users_router.post("/requests/{request_id}/reject", status_code=status.HTTP_204_NO_CONTENT, +@users_router.post("/requests/{request_id}/reject", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, dependencies=[Depends(require_admin)]) -async def reject_request(request_id: int, db: Session = Depends(get_session)): +async def reject_request(request_id: int, db: AsyncSession = Depends(get_session)): """ Reject a coach request """ - logic.reject_request(db, request_id) + await logic.reject_request(db, request_id) diff --git a/backend/src/app/schemas/answers.py b/backend/src/app/schemas/answers.py new file mode 100644 index 000000000..2ce370dad --- /dev/null +++ b/backend/src/app/schemas/answers.py @@ -0,0 +1,20 @@ +from src.app.schemas.utils import CamelCaseModel + + +class File(CamelCaseModel): + """question files""" + filename: str + mime_type: str + url: str + + +class QuestionAndAnswer(CamelCaseModel): + """question and answer""" + question: str + answers: list[str] + files: list[File] + + +class Questions(CamelCaseModel): + """return model of questions""" + q_and_a: list[QuestionAndAnswer] diff --git a/backend/src/app/schemas/editions.py b/backend/src/app/schemas/editions.py index 3b3618d0e..8f89ce5ea 100644 --- a/backend/src/app/schemas/editions.py +++ b/backend/src/app/schemas/editions.py @@ -22,6 +22,7 @@ class Edition(CamelCaseModel): edition_id: int name: str year: int + readonly: bool class Config: """Set to ORM mode""" @@ -35,3 +36,10 @@ class EditionList(CamelCaseModel): class Config: """Set to ORM mode""" orm_mode = True + + +class EditEdition(CamelCaseModel): + """Input schema to edit an edition + Only supported operation is patching the readonly status + """ + readonly: bool diff --git a/backend/src/app/schemas/login.py b/backend/src/app/schemas/login.py index b7e735e73..a4ca1e439 100644 --- a/backend/src/app/schemas/login.py +++ b/backend/src/app/schemas/login.py @@ -1,10 +1,11 @@ +from src.app.schemas.editions import Edition from src.app.schemas.users import User from src.app.schemas.utils import BaseModel class UserData(User): """User information that can be passed to frontend""" - editions: list[str] = [] + editions: list[Edition] = [] class Token(BaseModel): diff --git a/backend/src/app/schemas/oauth/__init__.py b/backend/src/app/schemas/oauth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/app/schemas/oauth/github.py b/backend/src/app/schemas/oauth/github.py new file mode 100644 index 000000000..2538ca6d2 --- /dev/null +++ b/backend/src/app/schemas/oauth/github.py @@ -0,0 +1,29 @@ +from pydantic import validator, BaseModel + +from src.app.exceptions.validation_exception import ValidationException + + +class AccessTokenResponse(BaseModel): + """Model for the response sent by GitHub when we request a user's access token""" + access_token: str + scope: str + + @validator("scope") + @classmethod + def split_scope(cls, scopes: str) -> str: + """Check if all the required scopes are present (users can deny them if they want to)""" + required_scopes = ["read:user", "user:email"] + provided_scopes = scopes.split(",") + + if not all(scope in provided_scopes for scope in required_scopes): + raise ValidationException("Missing scopes") + + return scopes + + +class GitHubProfile(BaseModel): + """Model for data we have about a user's GitHub profile""" + access_token: str + email: str + id: int + name: str diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index 93664116d..d2526880e 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -1,7 +1,10 @@ from dataclasses import dataclass -from pydantic import BaseModel +from pydantic import BaseModel, validator + +from src.app.schemas.skills import Skill from src.app.schemas.utils import CamelCaseModel +from src.app.schemas.validators import validate_url class User(CamelCaseModel): @@ -14,35 +17,62 @@ class Config: orm_mode = True -class Skill(CamelCaseModel): - """Represents a Skill from the database""" - skill_id: int +class Partner(CamelCaseModel): + """Represents a Partner from the database""" + partner_id: int name: str - description: str class Config: """Set to ORM mode""" orm_mode = True -class Partner(CamelCaseModel): +class Student(CamelCaseModel): """Represents a Partner from the database""" - partner_id: int - name: str + student_id: int + first_name: str + last_name: str class Config: """Set to ORM mode""" orm_mode = True +class ProjectRoleSuggestion(CamelCaseModel): + """Represents a ProjectRole from the database""" + project_role_suggestion_id: int + argumentation: str | None + drafter: User | None + student: Student | None + + class Config: + """Set to ORM mode""" + orm_mode = True + + +class ReturnProjectRoleSuggestion(CamelCaseModel): + """return a project role suggestion""" + project_role_suggestion: ProjectRoleSuggestion + + class ProjectRole(CamelCaseModel): """Represents a ProjectRole from the database""" - student_id: int + project_role_id: int project_id: int - skill_id: int - definitive: bool - argumentation: str | None - drafter_id: int + description: str | None + skill: Skill + slots: int + + suggestions: list[ProjectRoleSuggestion] + + class Config: + """Set to ORM mode""" + orm_mode = True + + +class ProjectRoleResponseList(CamelCaseModel): + """Response list containing project roles""" + project_roles: list[ProjectRole] class Config: """Set to ORM mode""" @@ -53,10 +83,9 @@ class Project(CamelCaseModel): """Represents a Project from the database to return when a GET request happens""" project_id: int name: str - number_of_students: int + info_url: str | None coaches: list[User] - skills: list[Skill] partners: list[Partner] project_roles: list[ProjectRole] @@ -65,36 +94,48 @@ class Config: orm_mode = True -class Student(CamelCaseModel): - """Represents a Student to use in ConflictStudent""" - student_id: int - first_name: str - last_name: str +class ProjectList(CamelCaseModel): + """A list of projects""" + projects: list[Project] + + +class ConflictProject(CamelCaseModel): + """A project to be used in ConflictStudent""" + project_id: int + name: str + info_url: str | None class Config: """Config Class""" orm_mode = True -class ConflictProject(CamelCaseModel): +class ConflictProjectRole(CamelCaseModel): """A project to be used in ConflictStudent""" - project_id: int - name: str + project_role_id: int + project: ConflictProject class Config: """Config Class""" orm_mode = True -class ProjectList(CamelCaseModel): - """A list of projects""" - projects: list[Project] +class ConflictRoleSuggestion(CamelCaseModel): + """Represents a ProjectRole from the database""" + project_role_suggestion_id: int + project_role: ConflictProjectRole + + class Config: + """Set to ORM mode""" + orm_mode = True class ConflictStudent(CamelCaseModel): """A student together with the projects they are causing a conflict for""" - student: Student - projects: list[ConflictProject] + student_id: int + first_name: str + last_name: str + pr_suggestions: list[ConflictRoleSuggestion] class Config: """Config Class""" @@ -106,18 +147,30 @@ class ConflictStudentList(CamelCaseModel): conflict_students: list[ConflictStudent] +class InputProjectRole(BaseModel): + """Used for creating a project role""" + skill_id: int + description: str | None + slots: int + + class InputProject(BaseModel): """Used for passing the details of a project when creating/patching a project""" name: str - number_of_students: int - skills: list[int] + info_url: str | None partners: list[str] coaches: list[int] + @validator('info_url') + @classmethod + def is_url(cls, info_url: str | None): + """Validate url""" + return validate_url(info_url) + -class InputStudentRole(BaseModel): +class InputArgumentation(BaseModel): """Used for creating/patching a student role""" - skill_id: int + argumentation: str | None @dataclass diff --git a/backend/src/app/schemas/register.py b/backend/src/app/schemas/register.py index e41e3d111..837250988 100644 --- a/backend/src/app/schemas/register.py +++ b/backend/src/app/schemas/register.py @@ -1,13 +1,17 @@ from uuid import UUID from src.app.schemas.invites import EmailAddress +from src.app.schemas.utils import CamelCaseModel -class NewUser(EmailAddress): - """ - The scheme of a new user - The email address will be stored in AuthEmail, but is included here to easily create a user - """ +class EmailRegister(EmailAddress): + """Scheme used for a new user created with email-password authentication""" name: str pw: str uuid: UUID + + +class GitHubRegister(CamelCaseModel): + """Scheme used for a new user created with GitHub OAuth""" + code: str + uuid: UUID diff --git a/backend/src/app/schemas/skills.py b/backend/src/app/schemas/skills.py index 9b56bfdce..9b1683b6e 100644 --- a/backend/src/app/schemas/skills.py +++ b/backend/src/app/schemas/skills.py @@ -4,14 +4,12 @@ class SkillBase(CamelCaseModel): """Schema of a skill""" name: str - description: str | None = None class Skill(CamelCaseModel): """Schema of a created skill""" skill_id: int name: str - description: str | None = None class Config: """Set to ORM mode""" diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index 0e7467c56..f74200cea 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -30,7 +30,7 @@ class Student(CamelCaseModel): student_id: int first_name: str last_name: str - preferred_name: str + preferred_name: str | None email_address: str phone_number: str alumni: bool @@ -68,6 +68,8 @@ class CommonQueryParams: alumni: bool = False student_coach: bool = False skill_ids: list[int] = Query([]) + own_suggestions: bool = False + decisions: list[DecisionEnum] = Query([]) page: int = 0 diff --git a/backend/src/app/schemas/suggestion.py b/backend/src/app/schemas/suggestion.py index db5b30eec..5340c1b34 100644 --- a/backend/src/app/schemas/suggestion.py +++ b/backend/src/app/schemas/suggestion.py @@ -17,7 +17,7 @@ class Suggestion(CamelCaseModel): """ suggestion_id: int - coach: User + coach: User | None suggestion: DecisionEnum argumentation: str @@ -42,7 +42,9 @@ class SuggestionResponse(CamelCaseModel): def suggestion_model_to_schema(suggestion_model: Suggestion_model) -> Suggestion: """Create Suggestion Schema from Suggestion Model""" - coach: User = user_model_to_schema(suggestion_model.coach) + coach: User | None = None + if suggestion_model.coach: + coach = user_model_to_schema(suggestion_model.coach) return Suggestion(suggestion_id=suggestion_model.suggestion_id, coach=coach, suggestion=suggestion_model.suggestion, diff --git a/backend/src/app/schemas/users.py b/backend/src/app/schemas/users.py index 27f8cabb1..266065c19 100644 --- a/backend/src/app/schemas/users.py +++ b/backend/src/app/schemas/users.py @@ -1,3 +1,5 @@ +from typing import Type + from src.app.schemas.editions import Edition from src.app.schemas.utils import CamelCaseModel, BaseModel from src.database.models import User as ModelUser @@ -9,6 +11,20 @@ class Authentication(CamelCaseModel): email: str +def get_user_model_auth(model_user: ModelUser) -> Authentication | None: + """Get a user's auth type""" + auth: Authentication | None = None + + if model_user.email_auth is not None: + auth = Authentication(auth_type="email", email=model_user.email_auth.email) + elif model_user.github_auth is not None: + auth = Authentication(auth_type="github", email=model_user.github_auth.email) + elif model_user.google_auth is not None: + auth = Authentication(auth_type="google", email=model_user.google_auth.email) + + return auth + + class User(CamelCaseModel): """Model for a user""" user_id: int @@ -16,6 +32,12 @@ class User(CamelCaseModel): admin: bool auth: Authentication | None + @classmethod + def from_orm(cls: Type['User'], obj: ModelUser) -> 'User': + """Override from_orm in order to instantiate the auth field""" + auth = get_user_model_auth(obj) + return cls(user_id=obj.user_id, name=obj.name, admin=obj.admin, auth=auth) + class Config: """Set to ORM mode""" orm_mode = True @@ -23,13 +45,7 @@ class Config: def user_model_to_schema(model_user: ModelUser) -> User: """Create User Schema from User Model""" - auth: Authentication | None = None - if model_user.email_auth is not None: - auth = Authentication(auth_type="email", email=model_user.email_auth.email) - elif model_user.github_auth is not None: - auth = Authentication(auth_type="github", email=model_user.github_auth.email) - elif model_user.google_auth is not None: - auth = Authentication(auth_type="google", email=model_user.google_auth.email) + auth = get_user_model_auth(model_user) return User( user_id=model_user.user_id, @@ -71,8 +87,8 @@ class UserRequestsResponse(CamelCaseModel): class FilterParameters(BaseModel): """Schema for query parameters""" - edition: str | None - exclude_edition: str | None - name: str | None - admin: bool | None + edition: str | None = None + exclude_edition: str | None = None + name: str | None = None + admin: bool | None = None page: int = 0 diff --git a/backend/src/app/schemas/validators.py b/backend/src/app/schemas/validators.py index 0884418cb..9064a5a0f 100644 --- a/backend/src/app/schemas/validators.py +++ b/backend/src/app/schemas/validators.py @@ -19,5 +19,14 @@ def validate_edition(edition: str): An edition should not contain any spaces in order for us to use it in the path of various resources, this function checks that. """ - if not re.fullmatch(r"\w+", edition): + if not re.fullmatch(r"[a-zA-Z0-9_-]+", edition): raise ValidationException("Spaces detected in the edition name") + + +def validate_url(info_url: str | None): + """Verify the info_url is actually an url""" + if not info_url: + return None + if info_url.startswith('https://') or info_url.startswith('http://'): + return info_url + raise ValidationException('info_url should be a link starting with http:// or https://') diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index 1e6043417..228608cf4 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -1,52 +1,67 @@ +from typing import AsyncGenerator + +import aiohttp import sqlalchemy.exc from fastapi import Depends from fastapi.security import OAuth2PasswordBearer from jose import jwt, ExpiredSignatureError, JWTError -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession import settings import src.database.crud.projects as crud_projects from src.app.exceptions.authentication import ( - ExpiredCredentialsException, InvalidCredentialsException, - MissingPermissionsException, WrongTokenTypeException) + ExpiredCredentialsException, + InvalidCredentialsException, + MissingPermissionsException, + WrongTokenTypeException +) from src.app.exceptions.editions import ReadOnlyEditionException +from src.app.exceptions.util import NotFound from src.app.logic.security import ALGORITHM, TokenType -from src.database.crud.editions import get_edition_by_name, latest_edition +from src.database.crud.editions import get_edition_by_name from src.database.crud.invites import get_invite_link_by_uuid from src.database.crud.students import get_student_by_id from src.database.crud.suggestions import get_suggestion_by_id from src.database.crud.users import get_user_by_id from src.database.database import get_session -from src.database.models import Edition, InviteLink, Student, Suggestion, User, Project +from src.database.models import Edition, InviteLink, Student, Suggestion, User, Project, ProjectRole -def get_edition(edition_name: str, database: Session = Depends(get_session)) -> Edition: +async def get_edition(edition_name: str, database: AsyncSession = Depends(get_session)) -> Edition: """Get an edition from the database, given the name in the path""" - return get_edition_by_name(database, edition_name) + return await get_edition_by_name(database, edition_name) -def get_student(student_id: int, database: Session = Depends(get_session)) -> Student: +async def get_student(student_id: int, database: AsyncSession = Depends(get_session), + edition: Edition = Depends(get_edition)) -> Student: """Get the student from the database, given the id in the path""" - return get_student_by_id(database, student_id) + student: Student = await get_student_by_id(database, student_id) + if student.edition != edition: + raise NotFound() + return student -def get_suggestion(suggestion_id: int, database: Session = Depends(get_session)) -> Suggestion: +async def get_suggestion(suggestion_id: int, database: AsyncSession = Depends(get_session), + student: Student = Depends(get_student)) -> Suggestion: """Get the suggestion from the database, given the id in the path""" - return get_suggestion_by_id(database, suggestion_id) + suggestion: Suggestion = await get_suggestion_by_id(database, suggestion_id) + if suggestion.student != student: + raise NotFound() + return suggestion -def get_latest_edition(edition: Edition = Depends(get_edition), database: Session = Depends(get_session)) -> Edition: - """Checks if the given edition is the latest one (others are read-only) and returns it if it is""" - latest = latest_edition(database) - if edition != latest: +async def get_editable_edition(edition: Edition = Depends(get_edition)) \ + -> Edition: + """Checks if the requested edition is editable, and returns it if it is""" + if edition.readonly: raise ReadOnlyEditionException - return latest + return edition -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login/token") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login/token/email") -async def _get_user_from_token(token_type: TokenType, db: Session, token: str) -> User: +async def _get_user_from_token(token_type: TokenType, db: AsyncSession, token: str) -> User: """Check which user is making a request by decoding its token, and verifying the token type""" try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM]) @@ -60,7 +75,7 @@ async def _get_user_from_token(token_type: TokenType, db: Session, token: str) - raise WrongTokenTypeException() try: - user = get_user_by_id(db, int(user_id)) + user = await get_user_by_id(db, int(user_id)) except sqlalchemy.exc.NoResultFound as not_found: raise InvalidCredentialsException() from not_found @@ -71,20 +86,27 @@ async def _get_user_from_token(token_type: TokenType, db: Session, token: str) - raise InvalidCredentialsException() from jwt_err -async def get_user_from_access_token(db: Session = Depends(get_session), token: str = Depends(oauth2_scheme)) -> User: +async def get_user_from_access_token(db: AsyncSession = Depends(get_session), + token: str = Depends(oauth2_scheme)) -> User: """Check which user is making a request by decoding its access token This function is used as a dependency for other functions """ return await _get_user_from_token(TokenType.ACCESS, db, token) -async def get_user_from_refresh_token(db: Session = Depends(get_session), token: str = Depends(oauth2_scheme)) -> User: +async def get_user_from_refresh_token(db: AsyncSession = Depends(get_session), token: str = Depends(oauth2_scheme)) \ + -> User: """Check which user is making a request by decoding its refresh token This function is used as a dependency for other functions """ return await _get_user_from_token(TokenType.REFRESH, db, token) +async def get_user_from_ws_token(token: str, db: AsyncSession = Depends(get_session)) -> User: + """Check which user is making a request by decoding its access token passed as query parameter""" + return await _get_user_from_token(TokenType.ACCESS, db, token) + + async def require_auth(user: User = Depends(get_user_from_access_token)) -> User: """Dependency to check if a user is at least a coach This dependency should be used to check for resources that aren't linked to @@ -128,11 +150,41 @@ async def require_coach(edition: Edition = Depends(get_edition), return user -def get_invite_link(invite_uuid: str, db: Session = Depends(get_session)) -> InviteLink: +async def require_coach_ws( + edition: Edition = Depends(get_edition), + user: User = Depends(get_user_from_ws_token)) -> User: + """Wrapper for require coach dependency for websockets""" + return await require_coach(edition, user) + + +async def get_invite_link(invite_uuid: str, db: AsyncSession = Depends(get_session)) -> InviteLink: """Get an invite link from the database, given the id in the path""" - return get_invite_link_by_uuid(db, invite_uuid) + return await get_invite_link_by_uuid(db, invite_uuid) -def get_project(project_id: int, db: Session = Depends(get_session)) -> Project: +async def get_project( + project_id: int, + db: AsyncSession = Depends(get_session), + edition: Edition = Depends(get_edition)) -> Project: """Get a project from het database, given the id in the path""" - return crud_projects.get_project(db, project_id) + project = await crud_projects.get_project(db, project_id) + if project.edition != edition: + raise NotFound() + return project + + +async def get_project_role( + project_role_id: int, + project: Project = Depends(get_project), + db: AsyncSession = Depends(get_session)) -> ProjectRole: + """Get a project from het database, given the id in the path""" + project_role = await crud_projects.get_project_role(db, project_role_id) + if project_role.project != project: + raise NotFound() + return project_role + + +async def get_http_session() -> AsyncGenerator[aiohttp.ClientSession, None]: + """Get an aiohttp ClientSession to send requests with""" + async with aiohttp.ClientSession() as session: # pragma: no cover + yield session diff --git a/backend/src/app/utils/websockets.py b/backend/src/app/utils/websockets.py new file mode 100644 index 000000000..bcccc7135 --- /dev/null +++ b/backend/src/app/utils/websockets.py @@ -0,0 +1,128 @@ +import enum +from asyncio import Queue as AsyncQueue, Lock +from enum import Enum +from queue import Queue + +from fastapi import Depends, Request, Response, FastAPI +from humps import camelize + +from src.app.utils.dependencies import get_edition +from src.database.models import Edition + + +@enum.unique +class EventType(Enum): + """Event Type Enum""" + # Project + PROJECT = 0 + # Project Role + PROJECT_ROLE = 1 + # Project Role Suggestion + PROJECT_ROLE_SUGGESTION = 2 + # Student + STUDENT = 3 + # Student Suggestion + STUDENT_SUGGESTION = 4 + + +class LiveEventParameters: + """Construct a live event data object""" + + def __init__(self, method: str, path_ids: dict): + """Initialize the object""" + self.method: str = method + self.path_ids: dict = path_ids + self.event_type: EventType = LiveEventParameters.get_event_type(path_ids) + + @staticmethod + def get_event_type(path_ids: dict) -> EventType: + """Parse the event type from given path ids""" + match path_ids: + case {'project_id': _, 'project_role_id': _, 'student_id': _}: + return EventType.PROJECT_ROLE_SUGGESTION + case {'project_id': _, 'project_role_id': _}: + return EventType.PROJECT_ROLE + case {'project_id': _}: + return EventType.PROJECT + case {'student_id': _, 'suggestion_id': _}: + return EventType.STUDENT_SUGGESTION + case {'student_id': _}: + return EventType.STUDENT + case _: + raise Exception('Invalid path_ids') + + async def json(self) -> dict: + """Generate json dict for live event""" + return { + 'method': self.method, + 'pathIds': {camelize(k): v for k, v in self.path_ids.items()}, + 'eventType': self.event_type.value + } + + +class DataPublisher: + """Event Bus""" + + def __init__(self): + """Initialize""" + self.queues: list[AsyncQueue] = [] + self._broadcast_lock: Lock = Lock() + + async def subscribe(self) -> AsyncQueue: + """Subscribe to the event bus, returns a queue where your events will be published""" + queue: AsyncQueue = AsyncQueue() + self.queues.append(queue) + return queue + + async def unsubscribe(self, queue: AsyncQueue) -> None: + """No longer receive updates""" + self.queues.remove(queue) + + async def broadcast(self, live_event: LiveEventParameters) -> None: + """Notify all subscribed listeners""" + data: dict = await live_event.json() + + async with self._broadcast_lock: + for queue in self.queues: + await queue.put(data) + + +# Map containing a publishers for each edition, since access is managed per edition +_publisher_by_edition: dict[str, DataPublisher] = {} + + +async def get_publisher(edition: Edition = Depends(get_edition)) -> DataPublisher: + """Get a publisher for the given edition""" + if edition.name not in _publisher_by_edition: + _publisher_by_edition[edition.name] = DataPublisher() + return _publisher_by_edition[edition.name] + + +async def live(request: Request, publisher: DataPublisher = Depends(get_publisher)): + """Add the publisher for the current edition to the queue + Indicates to the middleware the event might trigger a live data event + """ + queue: Queue = request.state.websocket_publisher_queue + queue.put_nowait(publisher) + + +def install_middleware(app: FastAPI): + """Middleware for sending actions upon successful requests to live endpoints""" + @app.middleware("http") + async def live_middleware(request: Request, call_next) -> Response: + queue: Queue[DataPublisher] = Queue() + request.state.websocket_publisher_queue = queue + + response: Response = await call_next(request) + + if 200 <= response.status_code < 300 and not queue.empty(): + if (publisher := queue.get_nowait()) is not None: + path_ids: dict = request.path_params.copy() + del path_ids['edition_name'] + live_event: LiveEventParameters = LiveEventParameters( + request.method, + path_ids + ) + await publisher.broadcast(live_event) + + return response diff --git a/backend/src/database/crud/editions.py b/backend/src/database/crud/editions.py index 1a29dcbec..ac147041e 100644 --- a/backend/src/database/crud/editions.py +++ b/backend/src/database/crud/editions.py @@ -1,12 +1,14 @@ -from sqlalchemy import exc, func -from sqlalchemy.orm import Query, Session -from src.app.exceptions.editions import DuplicateInsertException +from sqlalchemy import exc, select, desc +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.sql import Select + +from src.app.exceptions.crud import DuplicateInsertException from src.app.schemas.editions import EditionBase from src.database.models import Edition from .util import paginate -def get_edition_by_name(db: Session, edition_name: str) -> Edition: +async def get_edition_by_name(db: AsyncSession, edition_name: str) -> Edition: """Get an edition given its name Args: @@ -16,24 +18,28 @@ def get_edition_by_name(db: Session, edition_name: str) -> Edition: Returns: Edition: an edition if found else an exception is raised """ - return db.query(Edition).where(Edition.name == edition_name).one() + query = select(Edition).where(Edition.name == edition_name).order_by(desc(Edition.edition_id)) + result = await db.execute(query) + return result.scalars().one() -def _get_editions_query(db: Session) -> Query: - return db.query(Edition) +def _get_editions_query() -> Select: + return select(Edition).order_by(desc(Edition.year), desc(Edition.edition_id)) -def get_editions(db: Session) -> list[Edition]: +async def get_editions(db: AsyncSession) -> list[Edition]: """Returns a list of all editions""" - return _get_editions_query(db).all() + result = await db.execute(_get_editions_query()) + return result.scalars().all() -def get_editions_page(db: Session, page: int) -> list[Edition]: +async def get_editions_page(db: AsyncSession, page: int) -> list[Edition]: """Returns a paginated list of all editions""" - return paginate(_get_editions_query(db), page).all() + result = await db.execute(paginate(_get_editions_query(), page)) + return result.scalars().all() -def create_edition(db: Session, edition: EditionBase) -> Edition: +async def create_edition(db: AsyncSession, edition: EditionBase) -> Edition: """ Create a new edition. Args: @@ -46,26 +52,25 @@ def create_edition(db: Session, edition: EditionBase) -> Edition: new_edition: Edition = Edition(year=edition.year, name=edition.name) db.add(new_edition) try: - db.commit() - db.refresh(new_edition) + await db.commit() + await db.refresh(new_edition) return new_edition except exc.SQLAlchemyError as exception: raise DuplicateInsertException(exception) from exception -def delete_edition(db: Session, edition_name: str): +async def delete_edition(db: AsyncSession, edition_name: str): """Delete an edition. Args: db (Session): connection with the database. edition_name (str): the primary key of the edition that needs to be deleted """ - edition_to_delete = get_edition_by_name(db, edition_name) - db.delete(edition_to_delete) - db.commit() + await db.delete(await get_edition_by_name(db, edition_name)) + await db.commit() -def latest_edition(db: Session) -> Edition: - """Returns the latest edition from the database""" - max_edition_id = db.query(func.max(Edition.edition_id)).scalar() - return db.query(Edition).where(Edition.edition_id == max_edition_id).one() +async def patch_edition(db: AsyncSession, edition: Edition, readonly: bool): + """Update the readonly status of an edition""" + edition.readonly = readonly + await db.commit() diff --git a/backend/src/database/crud/invites.py b/backend/src/database/crud/invites.py index 9a794832b..fa36e8ab4 100644 --- a/backend/src/database/crud/invites.py +++ b/backend/src/database/crud/invites.py @@ -1,53 +1,57 @@ from uuid import UUID - -from sqlalchemy.orm import Session, Query +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.sql import Select from src.app.exceptions.parsing import MalformedUUIDError from src.database.crud.util import paginate from src.database.models import Edition, InviteLink -def create_invite_link(db: Session, edition: Edition, email_address: str) -> InviteLink: +async def create_invite_link(db: AsyncSession, edition: Edition, email_address: str) -> InviteLink: """Create a new invite link""" link = InviteLink(target_email=email_address, edition=edition) db.add(link) - db.commit() + await db.commit() return link -def delete_invite_link(db: Session, invite_link: InviteLink, commit: bool = True): +async def delete_invite_link(db: AsyncSession, invite_link: InviteLink, commit: bool = True): """Delete an invite link from the database""" - db.delete(invite_link) + await db.delete(invite_link) if commit: - db.commit() + await db.commit() -def _get_pending_invites_for_edition_query(db: Session, edition: Edition) -> Query: +def _get_pending_invites_for_edition_query(edition: Edition) -> Select: """Return the query for all InviteLinks linked to a given edition""" - return db.query(InviteLink).where(InviteLink.edition == edition).order_by(InviteLink.invite_link_id) + return select(InviteLink).where(InviteLink.edition == edition).order_by(InviteLink.invite_link_id) -def get_pending_invites_for_edition(db: Session, edition: Edition) -> list[InviteLink]: +async def get_pending_invites_for_edition(db: AsyncSession, edition: Edition) -> list[InviteLink]: """Returns a list with all InviteLinks linked to a given edition""" - return _get_pending_invites_for_edition_query(db, edition).all() + result = await db.execute(_get_pending_invites_for_edition_query(edition)) + return result.scalars().all() -def get_pending_invites_for_edition_page(db: Session, edition: Edition, page: int) -> list[InviteLink]: +async def get_pending_invites_for_edition_page(db: AsyncSession, edition: Edition, page: int) -> list[InviteLink]: """Returns a paginated list with all InviteLinks linked to a given edition""" - return paginate(_get_pending_invites_for_edition_query(db, edition), page).all() + result = await db.execute(paginate(_get_pending_invites_for_edition_query(edition), page)) + return result.scalars().all() -def get_optional_invite_link_by_edition_and_email(db: Session, edition: Edition, email: str) -> InviteLink | None: +async def get_optional_invite_link_by_edition_and_email(db: AsyncSession, edition: Edition, email: str) \ + -> InviteLink | None: """Return an optional invite link by edition and target_email""" - return db\ - .query(InviteLink)\ + query = select(InviteLink)\ .where(InviteLink.edition == edition)\ - .where(InviteLink.target_email == email)\ - .one_or_none() + .where(InviteLink.target_email == email) + result = await db.execute(query) + return result.scalars().one_or_none() -def get_invite_link_by_uuid(db: Session, invite_uuid: str | UUID) -> InviteLink: +async def get_invite_link_by_uuid(db: AsyncSession, invite_uuid: str | UUID) -> InviteLink: """Get an invite link by its id As the ids are auto-generated per row, there's no need to use the Edition from the path parameters as an extra filter @@ -60,4 +64,6 @@ def get_invite_link_by_uuid(db: Session, invite_uuid: str | UUID) -> InviteLink: # If conversion failed, then the input string was not a valid uuid raise MalformedUUIDError(str(invite_uuid)) from value_error - return db.query(InviteLink).where(InviteLink.uuid == invite_uuid).one() + query = select(InviteLink).where(InviteLink.uuid == invite_uuid) + result = await db.execute(query) + return result.scalars().one() diff --git a/backend/src/database/crud/partners.py b/backend/src/database/crud/partners.py new file mode 100644 index 000000000..ce0079510 --- /dev/null +++ b/backend/src/database/crud/partners.py @@ -0,0 +1,20 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from src.database.models import Partner + + +async def create_partner(db: AsyncSession, name: str, commit: bool = True) -> Partner: + """Create a partner given a name""" + partner = Partner(name=name) + db.add(partner) + + if commit: + await db.flush() + + return partner + + +async def get_optional_partner_by_name(db: AsyncSession, name: str) -> Partner | None: + """Returns an optional partner given a name""" + return (await db.execute(select(Partner).where(Partner.name == name))).unique().scalar_one_or_none() diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index 68ac90f66..1cdb30645 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -1,122 +1,160 @@ -from sqlalchemy.exc import NoResultFound -from sqlalchemy.orm import Session, Query +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.sql import Select -from src.app.schemas.projects import InputProject, QueryParamsProjects +import src.database.crud.skills as skills_crud +from src.app.schemas.projects import InputProject, InputProjectRole, QueryParamsProjects +from src.database.crud.users import get_user_by_id from src.database.crud.util import paginate -from src.database.models import Project, Edition, Student, ProjectRole, Skill, User, Partner +from src.database.models import Project, Edition, Student, ProjectRole, Partner, User -def _get_projects_for_edition_query(db: Session, edition: Edition) -> Query: - return db.query(Project).where(Project.edition == edition).order_by(Project.project_id) +def _get_projects_for_edition_query(edition: Edition) -> Select: + return select(Project).where(Project.edition == edition) -def get_projects_for_edition(db: Session, edition: Edition) -> list[Project]: +async def get_projects_for_edition(db: AsyncSession, edition: Edition) -> list[Project]: """Returns a list of all projects from a certain edition from the database""" - return _get_projects_for_edition_query(db, edition).all() + result = await db.execute(_get_projects_for_edition_query(edition).order_by(Project.name)) + return result.unique().scalars().all() -def get_projects_for_edition_page(db: Session, edition: Edition, - search_params: QueryParamsProjects, user: User) -> list[Project]: +async def get_projects_for_edition_page( + db: AsyncSession, + edition: Edition, + search_params: QueryParamsProjects, + user: User) -> list[Project]: """Returns a paginated list of all projects from a certain edition from the database""" - query = _get_projects_for_edition_query(db, edition).where( + query = _get_projects_for_edition_query(edition).where( Project.name.contains(search_params.name)) if search_params.coach: - query = query.where(Project.project_id.in_([user_project.project_id for user_project in user.projects])) - projects: list[Project] = paginate(query, search_params.page).all() - - return projects - - -def add_project(db: Session, edition: Edition, input_project: InputProject) -> Project: + query = query.where(Project.project_id.in_( + [user_project.project_id for user_project in user.projects])) + result = await db.execute(paginate(query.order_by(Project.name), search_params.page)) + return result.unique().scalars().all() + + +async def create_project( + db: AsyncSession, + edition: Edition, + input_project: InputProject, + partners: list[Partner], + commit: bool = True) -> Project: """ Add a project to the database If there are partner names that are not already in the database, add them """ - skills_obj = [db.query(Skill).where(Skill.skill_id == skill).one() - for skill in input_project.skills] - coaches_obj = [db.query(User).where(User.user_id == coach).one() - for coach in input_project.coaches] - partners_obj = [] - for partner in input_project.partners: - try: - partners_obj.append(db.query(Partner).where( - Partner.name == partner).one()) - except NoResultFound: - partner_obj = Partner(name=partner) - db.add(partner_obj) - partners_obj.append(partner_obj) - project = Project(name=input_project.name, number_of_students=input_project.number_of_students, - edition_id=edition.edition_id, skills=skills_obj, coaches=coaches_obj, partners=partners_obj) + coaches = [await get_user_by_id(db, coach) for coach in input_project.coaches] + + project = Project( + name=input_project.name, + info_url=input_project.info_url, + edition_id=edition.edition_id, + coaches=coaches, + partners=partners + ) db.add(project) - db.commit() + + if commit: + await db.commit() + return project -def get_project(db: Session, project_id: int) -> Project: +async def get_project(db: AsyncSession, project_id: int) -> Project: """Query a specific project from the database through its ID""" - return db.query(Project).where(Project.project_id == project_id).one() + query = select(Project).where(Project.project_id == project_id) + result = await db.execute(query) + project = result.unique().scalars().one() + # refresh to see updated relations + return project -def delete_project(db: Session, project_id: int): +async def delete_project(db: AsyncSession, project: Project): """Delete a specific project from the database""" - proj_roles = db.query(ProjectRole).where( - ProjectRole.project_id == project_id).all() - for proj_role in proj_roles: - db.delete(proj_role) - - project = get_project(db, project_id) - db.delete(project) - db.commit() + await db.delete(project) + await db.commit() -def patch_project(db: Session, project_id: int, input_project: InputProject): +async def patch_project( + db: AsyncSession, + project: Project, + input_project: InputProject, + partners: list[Partner], + commit: bool = True): """ Change some fields of a Project in the database If there are partner names that are not already in the database, add them """ - project = db.query(Project).where(Project.project_id == project_id).one() - - skills_obj = [db.query(Skill).where(Skill.skill_id == skill).one() - for skill in input_project.skills] - coaches_obj = [db.query(User).where(User.user_id == coach).one() - for coach in input_project.coaches] - partners_obj = [] - for partner in input_project.partners: - try: - partners_obj.append(db.query(Partner).where( - Partner.name == partner).one()) - except NoResultFound: - partner_obj = Partner(name=partner) - db.add(partner_obj) - partners_obj.append(partner_obj) + coaches = [await get_user_by_id(db, coach) for coach in input_project.coaches] project.name = input_project.name - project.number_of_students = input_project.number_of_students - project.skills = skills_obj - project.coaches = coaches_obj - project.partners = partners_obj - db.commit() + project.info_url = input_project.info_url + project.coaches = coaches + project.partners = partners + + if commit: + await db.commit() + + +async def get_project_role(db: AsyncSession, project_role_id: int) -> ProjectRole: + """Get a project role by id""" + return (await db.execute(select(ProjectRole).where(ProjectRole.project_role_id == project_role_id))).unique() \ + .scalar_one() + +async def get_project_roles_for_project(db: AsyncSession, project: Project) -> list[ProjectRole]: + """Get the project roles associated with a project""" + return (await db.execute(select(ProjectRole).where(ProjectRole.project == project))).unique().scalars().all() -def get_conflict_students(db: Session, edition: Edition) -> list[tuple[Student, list[Project]]]: + +async def create_project_role(db: AsyncSession, project: Project, input_project_role: InputProjectRole) -> ProjectRole: + """Create a project role for a project""" + skill = await skills_crud.get_skill_by_id(db, input_project_role.skill_id) + + project_role = ProjectRole( + project=project, + skill=skill, + description=input_project_role.description, + slots=input_project_role.slots + ) + + db.add(project_role) + await db.commit() + # query project_role to create association tables + (await db.execute(select(ProjectRole).where(ProjectRole.project_role_id == project_role.project_role_id))) + return project_role + + +async def patch_project_role( + db: AsyncSession, + project_role_id: int, + input_project_role: InputProjectRole) -> ProjectRole: + """Create a project role for a project""" + skill = await skills_crud.get_skill_by_id(db, input_project_role.skill_id) + project_role = await get_project_role(db, project_role_id) + + project_role.skill = skill + project_role.description = input_project_role.description + project_role.slots = input_project_role.slots + + await db.commit() + return project_role + + +async def get_conflict_students(db: AsyncSession, edition: Edition) -> list[Student]: """ - Query all students that are causing conflicts for a certain edition - Return a ConflictStudent for each student that causes a conflict - This class contains a student together with all projects they are causing a conflict for + Return an overview of the students that are assigned to multiple projects """ - students = db.query(Student).where(Student.edition == edition).all() - conflict_students = [] - projs = [] - for student in students: - if len(student.project_roles) > 1: - proj_ids = db.query(ProjectRole.project_id).where( - ProjectRole.student_id == student.student_id).all() - for proj_id in proj_ids: - proj_id = proj_id[0] - proj = db.query(Project).where( - Project.project_id == proj_id).one() - projs.append(proj) - conflict_student = (student, projs) - conflict_students.append(conflict_student) - return conflict_students + return [ + s for s in (await db.execute(select(Student).where(Student.edition == edition))).unique().scalars().all() + if len(s.pr_suggestions) > 1 + ] + + +async def delete_project_role(db: AsyncSession, project_role_id: int) -> None: + """delete a project role""" + project_role: ProjectRole = await get_project_role(db, project_role_id) + await db.delete(project_role) + await db.commit() diff --git a/backend/src/database/crud/projects_students.py b/backend/src/database/crud/projects_students.py index 5a3ea28b3..a87a83817 100644 --- a/backend/src/database/crud/projects_students.py +++ b/backend/src/database/crud/projects_students.py @@ -1,49 +1,71 @@ -from sqlalchemy.orm import Session +from sqlalchemy import select +from sqlalchemy.sql import Select +from sqlalchemy.ext.asyncio import AsyncSession +from src.app.schemas.projects import InputArgumentation +from src.database.models import ProjectRoleSuggestion -from src.database.models import Project, ProjectRole, Skill, User, Student +from src.database.models import ProjectRole, User, Student -def remove_student_project(db: Session, project: Project, student_id: int): - """Remove a student from a project in the database""" - proj_role = db.query(ProjectRole).where( - ProjectRole.student_id == student_id).where(ProjectRole.project == project).one() - db.delete(proj_role) - db.commit() - +def _get_pr_suggestion_for_pr_by_student_query(project_role: ProjectRole, student: Student) -> Select: + return select(ProjectRoleSuggestion).where( + ProjectRoleSuggestion.project_role == project_role).where( + ProjectRoleSuggestion.student == student + ) -def add_student_project(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): - """Add a student to a project in the database""" - # check if all parameters exist in the database - db.query(Skill).where(Skill.skill_id == skill_id).one() - db.query(User).where(User.user_id == drafter_id).one() - db.query(Student).where(Student.student_id == student_id).one() +async def get_pr_suggestion_for_pr_by_student( + db: AsyncSession, + project_role: ProjectRole, + student: Student) -> ProjectRoleSuggestion: + """Get the project role suggestion for a student""" + return (await db.execute(_get_pr_suggestion_for_pr_by_student_query(project_role, student))).scalar_one() - proj_role = ProjectRole(student_id=student_id, project_id=project.project_id, skill_id=skill_id, - drafter_id=drafter_id) - db.add(proj_role) - db.commit() +async def get_optional_pr_suggestion_for_pr_by_student( + db: AsyncSession, + project_role: ProjectRole, + student: Student) -> ProjectRoleSuggestion | None: + """Get the project role suggestion for a student, but don't raise an error when none is found""" + return (await db.execute(_get_pr_suggestion_for_pr_by_student_query(project_role, student))).scalar_one_or_none() -def change_project_role(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): - """Change the role of a student in a project and update the drafter""" - # check if all parameters exist in the database - db.query(Skill).where(Skill.skill_id == skill_id).one() - db.query(User).where(User.user_id == drafter_id).one() - db.query(Student).where(Student.student_id == student_id).one() +async def create_pr_suggestion( + db: AsyncSession, + project_role: ProjectRole, + student: Student, + drafter: User, + argumentation: InputArgumentation) -> ProjectRoleSuggestion: + """Create a project role suggestion""" + pr_suggestion = ProjectRoleSuggestion( + project_role=project_role, + student=student, + drafter=drafter, + argumentation=argumentation.argumentation + ) + db.add(pr_suggestion) + await db.commit() + return pr_suggestion - proj_role = db.query(ProjectRole).where( - ProjectRole.student_id == student_id).where(ProjectRole.project == project).one() - proj_role.drafter_id = drafter_id - proj_role.skill_id = skill_id - db.commit() +async def update_pr_suggestion( + db: AsyncSession, + pr_suggestion: ProjectRoleSuggestion, + drafter: User, + argumentation: InputArgumentation) -> ProjectRoleSuggestion: + """Update a project role suggestion""" + pr_suggestion.argumentation = argumentation.argumentation + pr_suggestion.drafter = drafter + await db.commit() + return pr_suggestion -def confirm_project_role(db: Session, project: Project, student_id: int): - """Confirm a project role""" - proj_role = db.query(ProjectRole).where(ProjectRole.student_id == student_id) \ - .where(ProjectRole.project == project).one() - proj_role.definitive = True - db.commit() +async def remove_project_role_suggestion(db: AsyncSession, project_role: ProjectRole, student: Student): + """Remove a student from a project in the database""" + project_role_suggestion = (await db.execute(select(ProjectRoleSuggestion).where( + ProjectRoleSuggestion.student == student).where( + ProjectRoleSuggestion.project_role == project_role + ))).scalar_one() + await db.delete(project_role_suggestion) + await db.commit() + await db.refresh(project_role) diff --git a/backend/src/database/crud/register.py b/backend/src/database/crud/register.py index 427a2264a..fc3fdb2ca 100644 --- a/backend/src/database/crud/register.py +++ b/backend/src/database/crud/register.py @@ -1,36 +1,48 @@ -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession -from src.database.models import AuthEmail, CoachRequest, User, Edition +from src.app.schemas.oauth.github import GitHubProfile +from src.database.models import AuthEmail, CoachRequest, User, Edition, AuthGitHub -def create_user(db: Session, name: str, commit: bool = True) -> User: +async def create_user(db: AsyncSession, name: str, commit: bool = True) -> User: """Create a user""" new_user: User = User(name=name) db.add(new_user) if commit: - db.commit() + await db.commit() return new_user -def create_coach_request(db: Session, user: User, edition: Edition, commit: bool = True) -> CoachRequest: +async def create_coach_request(db: AsyncSession, user: User, edition: Edition, commit: bool = True) -> CoachRequest: """Create a coach request""" coach_request: CoachRequest = CoachRequest(user=user, edition=edition) db.add(coach_request) if commit: - db.commit() + await db.commit() return coach_request -def create_auth_email(db: Session, user: User, pw_hash: str, email: str, commit: bool = True) -> AuthEmail: - """Create a authentication for email""" +async def create_auth_email(db: AsyncSession, user: User, pw_hash: str, email: str, commit: bool = True) -> AuthEmail: + """Create an authentication entry for email-password""" auth_email: AuthEmail = AuthEmail(user=user, pw_hash=pw_hash, email=email) db.add(auth_email) if commit: - db.commit() + await db.commit() return auth_email + + +async def create_auth_github(db: AsyncSession, user: User, profile: GitHubProfile, commit: bool = True) -> AuthGitHub: + """Create an authentication entry for GitHub""" + auth_gh = AuthGitHub(user=user, access_token=profile.access_token, email=profile.email, github_user_id=profile.id) + db.add(auth_gh) + + if commit: + await db.commit() + + return auth_gh diff --git a/backend/src/database/crud/skills.py b/backend/src/database/crud/skills.py index 4d9e72289..b51d9d6b1 100644 --- a/backend/src/database/crud/skills.py +++ b/backend/src/database/crud/skills.py @@ -1,10 +1,11 @@ -from sqlalchemy.orm import Session +from sqlalchemy import select, delete +from sqlalchemy.ext.asyncio import AsyncSession from src.app.schemas.skills import SkillBase from src.database.models import Skill -def get_skills(db: Session) -> list[Skill]: +async def get_skills(db: AsyncSession) -> list[Skill]: """Get a list of all the base skills that can be added to a student or project. Args: @@ -13,38 +14,46 @@ def get_skills(db: Session) -> list[Skill]: Returns: SkillList: an object with a list of all the skills. """ - return db.query(Skill).all() + return (await db.execute(select(Skill))).scalars().all() -def get_skills_by_ids(db: Session, skill_ids) -> list[Skill]: +async def get_skills_by_ids(db: AsyncSession, skill_ids) -> list[Skill]: """Get all skills from list of skill ids""" - return db.query(Skill).where(Skill.skill_id.in_(skill_ids)).all() + return (await db.execute(select(Skill).where(Skill.skill_id.in_(skill_ids)))).scalars().all() -def create_skill(db: Session, skill: SkillBase) -> Skill: - """Add a new skill into the database. +async def get_skill_by_id(db: AsyncSession, skill_id: int) -> Skill: + """Get a skill for a id""" + return (await db.execute(select(Skill).where(Skill.skill_id == skill_id))).scalar_one() - Args: - db (Session): connection with the database. - skill (SkillBase): has all the fields needed to add a skill. - Returns: - Skill: returns the new skill. - """ - new_skill: Skill = Skill(name=skill.name, description=skill.description) +async def get_skill_by_name(db: AsyncSession, skill_name: str) -> Skill: + """Get a skill by name""" + return (await db.execute(select(Skill).where(Skill.name == skill_name))).scalar_one() + +async def create_skill(db: AsyncSession, skill: SkillBase) -> Skill: + """Add a new skill into the database.""" + new_skill: Skill = Skill(name=skill.name) db.add(new_skill) - db.commit() - db.refresh(new_skill) + await db.commit() + await db.refresh(new_skill) return new_skill -def delete_skill(db: Session, skill_id: int): - """Delete an existing skill. +async def delete_skill(db: AsyncSession, skill_id: int): + """Delete an existing skill.""" + # query a skill to return 404 if it doesn't exist + (await (db.execute(select(Skill).where(Skill.skill_id == skill_id)))).scalars().one() + await db.execute(delete(Skill).where(Skill.skill_id == skill_id)) + await db.commit() - Args: - db (Session): connection with the database. - skill_id (int): the id of the skill - """ - skill_to_delete = db.query(Skill).where(Skill.skill_id == skill_id).one() - db.delete(skill_to_delete) - db.commit() + +async def create_skill_if_not_present(db: AsyncSession, name: str) -> bool: + """Create a skill if it doesn't exist""" + existing = (await db.execute(select(Skill).where(Skill.name == name))).one_or_none() + if existing: + return False + + # Create the skill + await create_skill(db, SkillBase(name=name)) + return True diff --git a/backend/src/database/crud/students.py b/backend/src/database/crud/students.py index 17d61aa43..fb6ba3edb 100644 --- a/backend/src/database/crud/students.py +++ b/backend/src/database/crud/students.py @@ -1,37 +1,41 @@ from datetime import datetime -from sqlalchemy.orm import Session -from sqlalchemy.sql.expression import func +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.sql.expression import func, and_, desc + +from src.app.schemas.students import CommonQueryParams, EmailsSearchQueryParams + from src.database.crud.util import paginate from src.database.enums import DecisionEnum, EmailStatusEnum -from src.database.models import Edition, Skill, Student, DecisionEmail -from src.app.schemas.students import CommonQueryParams, EmailsSearchQueryParams +from src.database.models import Edition, Skill, Student, DecisionEmail, Suggestion, User -def get_student_by_id(db: Session, student_id: int) -> Student: +async def get_student_by_id(db: AsyncSession, student_id: int) -> Student: """Get a student by id""" + query = select(Student).where(Student.student_id == student_id) + result = await db.execute(query) + return result.unique().scalars().one() - return db.query(Student).where(Student.student_id == student_id).one() - -def set_definitive_decision_on_student(db: Session, student: Student, decision: DecisionEnum) -> None: +async def set_definitive_decision_on_student(db: AsyncSession, student: Student, decision: DecisionEnum) -> None: """set a definitive decision on a student""" student.decision = decision - db.commit() + await db.commit() -def delete_student(db: Session, student: Student) -> None: +async def delete_student(db: AsyncSession, student: Student) -> None: """Delete a student from the database""" - db.delete(student) - db.commit() + await db.delete(student) + await db.commit() -def get_students(db: Session, edition: Edition, - commons: CommonQueryParams, skills: list[Skill] = None) -> list[Student]: +async def get_students(db: AsyncSession, edition: Edition, + commons: CommonQueryParams, user: User, skills: list[Skill] = None) -> list[Student]: """Get students""" - query = db.query(Student)\ - .where(Student.edition == edition)\ - .where((Student.first_name + ' ' + Student.last_name).contains(commons.name))\ + query = select(Student) \ + .where(Student.edition == edition) \ + .where((Student.first_name + ' ' + Student.last_name).contains(commons.name)) if commons.alumni: query = query.where(Student.alumni) @@ -39,43 +43,57 @@ def get_students(db: Session, edition: Edition, if commons.student_coach: query = query.where(Student.wants_to_be_student_coach) + if commons.own_suggestions: + subquery = select(Suggestion.student_id).where(Suggestion.coach == user) + query = query.filter(Student.student_id.in_(subquery)) + + if commons.decisions: + query = query.where(Student.decision.in_(commons.decisions)) + if skills is None: skills = [] for skill in skills: query = query.where(Student.skills.contains(skill)) - return paginate(query, commons.page).all() + query = query.order_by(Student.first_name, Student.last_name) + return (await db.execute(paginate(query, commons.page))).unique().scalars().all() -def get_emails(db: Session, student: Student) -> list[DecisionEmail]: +async def get_emails(db: AsyncSession, student: Student) -> list[DecisionEmail]: """Get all emails send to a student""" - return db.query(DecisionEmail).where(DecisionEmail.student_id == student.student_id).all() + query = select(DecisionEmail)\ + .where(DecisionEmail.student_id == student.student_id)\ + .order_by(desc(DecisionEmail.date)) + result = await db.execute(query) + return result.unique().scalars().all() -def create_email(db: Session, student: Student, email_status: EmailStatusEnum) -> DecisionEmail: +async def create_email(db: AsyncSession, student: Student, email_status: EmailStatusEnum) -> DecisionEmail: """Create a new email in the database""" email: DecisionEmail = DecisionEmail( student=student, decision=email_status, date=datetime.now()) db.add(email) - db.commit() + await db.commit() return email -def get_last_emails_of_students(db: Session, edition: Edition, commons: EmailsSearchQueryParams) -> list[DecisionEmail]: +async def get_last_emails_of_students(db: AsyncSession, edition: Edition, commons: EmailsSearchQueryParams) -> list[ + DecisionEmail]: """get last email of all students that got an email""" - last_emails = db.query(DecisionEmail.email_id, func.max(DecisionEmail.date))\ - .join(Student)\ - .where(Student.edition == edition)\ - .where((Student.first_name + ' ' + Student.last_name).contains(commons.name))\ - .group_by(DecisionEmail.student_id).subquery() + last_emails = select(DecisionEmail.student_id, func.max(DecisionEmail.date).label("maxdate")) \ + .join(Student) \ + .where(Student.edition == edition) \ + .where((Student.first_name + ' ' + Student.last_name).contains(commons.name)) \ + .group_by(DecisionEmail.student_id).subquery() - emails = db.query(DecisionEmail).join( - last_emails, DecisionEmail.email_id == last_emails.c.email_id - ) + emails = select(DecisionEmail).join( + last_emails, and_(DecisionEmail.student_id == last_emails.c.student_id, + DecisionEmail.date == last_emails.c.maxdate) + ) if commons.email_status: emails = emails.where(DecisionEmail.decision.in_(commons.email_status)) - emails = emails.order_by(DecisionEmail.student_id) - return paginate(emails, commons.page).all() + emails = emails.join(Student, DecisionEmail.student).order_by(Student.first_name, Student.last_name) + return (await db.execute(paginate(emails, commons.page))).unique().scalars().all() diff --git a/backend/src/database/crud/suggestions.py b/backend/src/database/crud/suggestions.py index be3853b8d..633433bec 100644 --- a/backend/src/database/crud/suggestions.py +++ b/backend/src/database/crud/suggestions.py @@ -1,57 +1,65 @@ -from sqlalchemy.orm import Session +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from src.database.models import Suggestion from src.database.enums import DecisionEnum -def create_suggestion(db: Session, user_id: int | None, student_id: int | None, - decision: DecisionEnum, argumentation: str) -> Suggestion: +async def create_suggestion(db: AsyncSession, user_id: int | None, student_id: int | None, + decision: DecisionEnum, argumentation: str) -> Suggestion: """ Create a new suggestion in the database """ suggestion: Suggestion = Suggestion( student_id=student_id, coach_id=user_id, suggestion=decision, argumentation=argumentation) db.add(suggestion) - db.commit() + await db.commit() return suggestion -def get_suggestions_of_student(db: Session, student_id: int | None) -> list[Suggestion]: +async def get_suggestions_of_student(db: AsyncSession, student_id: int | None) -> list[Suggestion]: """Give all suggestions of a student""" - return db.query(Suggestion).where(Suggestion.student_id == student_id).all() + query = select(Suggestion).where(Suggestion.student_id == student_id) + result = await db.execute(query) + return result.unique().scalars().all() -def get_own_suggestion(db: Session, student_id: int | None, user_id: int | None) -> Suggestion | None: +async def get_own_suggestion(db: AsyncSession, student_id: int | None, user_id: int | None) -> Suggestion | None: """Get the suggestion you made for a student""" # This isn't even possible but it pleases Mypy if student_id is None or user_id is None: return None - return db.query(Suggestion).where(Suggestion.student_id == student_id).where( - Suggestion.coach_id == user_id).one_or_none() + query = select(Suggestion).where(Suggestion.student_id == student_id).where( + Suggestion.coach_id == user_id) + result = await db.execute(query) + return result.unique().scalar_one_or_none() -def get_suggestion_by_id(db: Session, suggestion_id: int) -> Suggestion: +async def get_suggestion_by_id(db: AsyncSession, suggestion_id: int) -> Suggestion: """Give a suggestion based on the ID""" - return db.query(Suggestion).where(Suggestion.suggestion_id == suggestion_id).one() + result = await db.execute(select(Suggestion).where(Suggestion.suggestion_id == suggestion_id)) + return result.unique().scalar_one() -def delete_suggestion(db: Session, suggestion: Suggestion) -> None: +async def delete_suggestion(db: AsyncSession, suggestion: Suggestion) -> None: """Delete a suggestion from the database""" - db.delete(suggestion) - db.commit() + await db.delete(suggestion) + await db.commit() -def update_suggestion(db: Session, suggestion: Suggestion, decision: DecisionEnum, argumentation: str) -> None: +async def update_suggestion(db: AsyncSession, suggestion: Suggestion, decision: DecisionEnum, + argumentation: str) -> None: """Update a suggestion""" suggestion.suggestion = decision suggestion.argumentation = argumentation - db.commit() + await db.commit() -def get_suggestions_of_student_by_type(db: Session, student_id: int | None, - type_suggestion: DecisionEnum) -> list[Suggestion]: +async def get_suggestions_of_student_by_type(db: AsyncSession, student_id: int | None, + type_suggestion: DecisionEnum) -> list[Suggestion]: """Give all suggestions of a student by type""" - return db.query(Suggestion) \ - .where(Suggestion.student_id == student_id) \ - .where(Suggestion.suggestion == type_suggestion).all() + query = select(Suggestion).where(Suggestion.student_id == student_id)\ + .where(Suggestion.suggestion == type_suggestion) + result = await db.execute(query) + return result.unique().scalars().all() diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index d8091f1f2..21df9a718 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -1,4 +1,6 @@ -from sqlalchemy.orm import Session, Query +from sqlalchemy import select, delete +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.sql import Select from src.app.schemas.users import FilterParameters from src.database.crud.editions import get_edition_by_name @@ -7,36 +9,21 @@ from src.database.models import user_editions, User, Edition, CoachRequest, AuthEmail, AuthGitHub, AuthGoogle -def get_user_edition_names(db: Session, user: User) -> list[str]: +async def get_user_editions(db: AsyncSession, user: User) -> list[Edition]: """Get all names of the editions this user can see""" # For admins: return all editions - otherwise, all editions this user is verified coach in - source = user.editions if not user.admin else get_editions(db) + # Sort by year first, id second, descending + return sorted(user.editions, key=lambda x: (x.year, x.edition_id), + reverse=True) if not user.admin else await get_editions(db) - editions = [] - # Name & year are non-nullable in the database, so it can never be None, - # but MyPy doesn't seem to grasp that concept just yet so we have to check it - # Could be a oneliner/list comp but that's a bit less readable - # Return from newest to oldest - for edition in sorted(source, key=lambda e: e.year or -1, reverse=True): - if edition.name is not None: - editions.append(edition.name) - return editions - - -def get_users_filtered_page(db: Session, params: FilterParameters): +async def get_users_filtered_page(db: AsyncSession, params: FilterParameters): """ Get users and filter by optional parameters: - :param admin: only return admins / only return non-admins - :param edition_name: only return users who are coach of the given edition - :param exclude_edition_name: only return users who are not coach of the given edition - :param name: a string which the user's name must contain - :param page: the page to return - Note: When the admin parameter is set, edition_name and exclude_edition_name will be ignored. """ - query = db.query(User) + query = select(User) if params.name is not None: query = query.where(User.name.contains(params.name)) @@ -44,87 +31,92 @@ def get_users_filtered_page(db: Session, params: FilterParameters): if params.admin is not None: query = query.filter(User.admin.is_(params.admin)) # If admin parameter is set, edition & exclude_edition is ignored - return paginate(query, params.page).all() + return (await db.execute(paginate(query, params.page))).unique().scalars().all() if params.edition is not None: - edition = get_edition_by_name(db, params.edition) + edition = await get_edition_by_name(db, params.edition) query = query \ .join(user_editions) \ .filter(user_editions.c.edition_id == edition.edition_id) if params.exclude_edition is not None: - exclude_edition = get_edition_by_name(db, params.exclude_edition) + exclude_edition = await get_edition_by_name(db, params.exclude_edition) + exclude_user_id = select(user_editions.c.user_id) \ + .where(user_editions.c.edition_id == exclude_edition.edition_id) - query = query.filter( - User.user_id.not_in( - db.query(user_editions.c.user_id).where(user_editions.c.edition_id == exclude_edition.edition_id) - ) - ) + query = query.filter(User.user_id.not_in(exclude_user_id)) - return paginate(query, params.page).all() + query = query.order_by(User.name) + return (await db.execute(paginate(query, params.page))).unique().scalars().all() -def edit_admin_status(db: Session, user_id: int, admin: bool): +async def edit_admin_status(db: AsyncSession, user_id: int, admin: bool): """ Edit the admin-status of a user """ - user = db.query(User).where(User.user_id == user_id).one() + result = await db.execute(select(User).where(User.user_id == user_id)) + user = result.unique().scalar_one() user.admin = admin db.add(user) - db.commit() + await db.commit() -def add_coach(db: Session, user_id: int, edition_name: str): +async def add_coach(db: AsyncSession, user_id: int, edition_name: str): """ Add user as coach for the given edition """ - user = db.query(User).where(User.user_id == user_id).one() - edition = db.query(Edition).where(Edition.name == edition_name).one() + user_result = await db.execute(select(User).where(User.user_id == user_id)) + user = user_result.unique().scalar_one() + edition_result = await db.execute(select(Edition).where(Edition.name == edition_name)) + edition = edition_result.scalar_one() user.editions.append(edition) - db.commit() + + await db.commit() -def remove_coach(db: Session, user_id: int, edition_name: str): +async def remove_coach(db: AsyncSession, user_id: int, edition_name: str): """ Remove user as coach for the given edition """ - edition = db.query(Edition).where(Edition.name == edition_name).one() - db.query(user_editions) \ + edition_result = await db.execute(select(Edition).where(Edition.name == edition_name)) + edition = edition_result.scalar_one() + + delete_query = delete(user_editions) \ .where(user_editions.c.user_id == user_id) \ - .where(user_editions.c.edition_id == edition.edition_id) \ - .delete() - db.commit() + .where(user_editions.c.edition_id == edition.edition_id) + await db.execute(delete_query) + await db.commit() -def remove_coach_all_editions(db: Session, user_id: int): +async def remove_coach_all_editions(db: AsyncSession, user_id: int): """ Remove user as coach from all editions """ - db.query(user_editions).where(user_editions.c.user_id == user_id).delete() - db.commit() + await db.execute(delete(user_editions).where(user_editions.c.user_id == user_id)) + await db.commit() -def _get_requests_query(db: Session, user_name: str = "") -> Query: - return db.query(CoachRequest).join(User).where(User.name.contains(user_name)) +def _get_requests_query(user_name: str = "") -> Select: + return select(CoachRequest).join(User).where(User.name.contains(user_name)) -def get_requests(db: Session) -> list[CoachRequest]: +async def get_requests(db: AsyncSession) -> list[CoachRequest]: """ Get all userrequests """ - return _get_requests_query(db).all() + return (await db.execute(_get_requests_query())).unique().scalars().all() -def get_requests_page(db: Session, page: int, user_name: str = "") -> list[CoachRequest]: +async def get_requests_page(db: AsyncSession, page: int, user_name: str = "") -> list[CoachRequest]: """ Get all userrequests """ - return paginate(_get_requests_query(db, user_name), page).all() + return (await db.execute(paginate(_get_requests_query(user_name), page))).unique().scalars().all() -def _get_requests_for_edition_query(db: Session, edition: Edition, user_name: str = "") -> Query: - return db.query(CoachRequest) \ +def _get_requests_for_edition_query(edition: Edition, user_name: str = "") -> Select: + return select(CoachRequest) \ .where(CoachRequest.edition_id == edition.edition_id) \ .join(User) \ .where(User.name.contains(user_name)) \ @@ -133,15 +125,16 @@ def _get_requests_for_edition_query(db: Session, edition: Edition, user_name: st .join(AuthGoogle, isouter=True) -def get_requests_for_edition(db: Session, edition_name: str = "") -> list[CoachRequest]: +async def get_requests_for_edition(db: AsyncSession, edition_name: str = "") -> list[CoachRequest]: """ Get all userrequests from a given edition """ - return _get_requests_for_edition_query(db, get_edition_by_name(db, edition_name)).all() + edition = await get_edition_by_name(db, edition_name) + return (await db.execute(_get_requests_for_edition_query(edition))).unique().scalars().all() -def get_requests_for_edition_page( - db: Session, +async def get_requests_for_edition_page( + db: AsyncSession, edition_name: str, page: int, user_name: str = "" @@ -149,41 +142,54 @@ def get_requests_for_edition_page( """ Get all userrequests from a given edition """ - return paginate(_get_requests_for_edition_query(db, get_edition_by_name(db, edition_name), user_name), page).all() + edition = await get_edition_by_name(db, edition_name) + return \ + (await db.execute(paginate(_get_requests_for_edition_query(edition, user_name), page))).unique().scalars().all() -def accept_request(db: Session, request_id: int): +async def accept_request(db: AsyncSession, request_id: int): """ Remove request and add user as coach """ - request = db.query(CoachRequest).where(CoachRequest.request_id == request_id).one() - edition = db.query(Edition).where(Edition.edition_id == request.edition_id).one() - add_coach(db, request.user_id, edition.name) - db.query(CoachRequest).where(CoachRequest.request_id == request_id).delete() - db.commit() + request = \ + (await db.execute(select(CoachRequest).where(CoachRequest.request_id == request_id))).unique().scalar_one() + edition = (await db.execute(select(Edition).where(Edition.edition_id == request.edition_id))).scalar_one() + await add_coach(db, request.user_id, edition.name) + await db.execute(delete(CoachRequest).where(CoachRequest.request_id == request_id)) + await db.commit() -def reject_request(db: Session, request_id: int): +async def reject_request(db: AsyncSession, request_id: int): """ Remove request """ - db.query(CoachRequest).where(CoachRequest.request_id == request_id).delete() - db.commit() + await db.execute(delete(CoachRequest).where(CoachRequest.request_id == request_id)) + await db.commit() -def remove_request_if_exists(db: Session, user_id: int, edition_name: str): +async def remove_request_if_exists(db: AsyncSession, user_id: int, edition_name: str): """Remove a pending request for a user if there is one, otherwise do nothing""" - edition = db.query(Edition).where(Edition.name == edition_name).one() - db.query(CoachRequest).where(CoachRequest.user_id == user_id)\ - .where(CoachRequest.edition_id == edition.edition_id).delete() + edition = (await db.execute(select(Edition).where(Edition.name == edition_name))).scalar_one() + delete_query = delete(CoachRequest).where(CoachRequest.user_id == user_id) \ + .where(CoachRequest.edition_id == edition.edition_id) + await db.execute(delete_query) + await db.commit() -def get_user_by_email(db: Session, email: str) -> User: +async def get_user_by_email(db: AsyncSession, email: str) -> User: """Find a user by their email address""" - auth_email = db.query(AuthEmail).where(AuthEmail.email == email).one() - return db.query(User).where(User.user_id == auth_email.user_id).one() + auth_email = (await db.execute(select(AuthEmail).where(AuthEmail.email == email))).scalar_one() + return (await db.execute(select(User).where(User.user_id == auth_email.user_id))).unique().scalar_one() -def get_user_by_id(db: Session, user_id: int) -> User: +async def get_user_by_id(db: AsyncSession, user_id: int) -> User: """Find a user by their id""" - return db.query(User).where(User.user_id == user_id).one() + query = select(User).where(User.user_id == user_id) + result = await db.execute(query) + return result.unique().scalars().one() + + +async def get_user_by_github_id(db: AsyncSession, github_id: int) -> User: + """Find a user by their GitHub id""" + auth_gh = (await db.execute(select(AuthGitHub).where(AuthGitHub.github_user_id == github_id))).scalar_one() + return await get_user_by_id(db, auth_gh.user_id) diff --git a/backend/src/database/crud/util.py b/backend/src/database/crud/util.py index ca4a97020..90e153a04 100644 --- a/backend/src/database/crud/util.py +++ b/backend/src/database/crud/util.py @@ -1,8 +1,8 @@ -from sqlalchemy.orm import Query +from sqlalchemy.sql import Select import settings -def paginate(query: Query, page: int) -> Query: +def paginate(query: Select, page: int) -> Select: """Given a query, apply pagination and return the given page based on the page size""" return query.slice(page * settings.DB_PAGE_SIZE, (page + 1) * settings.DB_PAGE_SIZE) diff --git a/backend/src/database/crud/webhooks.py b/backend/src/database/crud/webhooks.py index f2e25e98b..cbcfcbda6 100644 --- a/backend/src/database/crud/webhooks.py +++ b/backend/src/database/crud/webhooks.py @@ -1,16 +1,17 @@ -from sqlalchemy.orm import Session +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from src.database.models import WebhookURL, Edition -def get_webhook(database: Session, uuid: str) -> WebhookURL: +async def get_webhook(database: AsyncSession, uuid: str) -> WebhookURL: """Retrieve a webhook by uuid""" - return database.query(WebhookURL).where(WebhookURL.uuid == uuid).one() + return (await database.execute(select(WebhookURL).where(WebhookURL.uuid == uuid))).scalar_one() -def create_webhook(database: Session, edition: Edition) -> WebhookURL: +async def create_webhook(database: AsyncSession, edition: Edition) -> WebhookURL: """Create a webhook for a given edition""" webhook_url: WebhookURL = WebhookURL(edition=edition) database.add(webhook_url) - database.commit() + await database.commit() return webhook_url diff --git a/backend/src/database/database.py b/backend/src/database/database.py index 6b2cf04cf..976a48fa5 100644 --- a/backend/src/database/database.py +++ b/backend/src/database/database.py @@ -1,9 +1,9 @@ -from typing import Generator +from typing import AsyncGenerator -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession -def get_session() -> Generator[Session, None, None]: +async def get_session() -> AsyncGenerator[AsyncSession, None]: # pragma: no cover """FastAPI dependency to inject a database session into a route instead of using an import Allows the tests to replace it with another database session (not hard coding the session) """ @@ -18,4 +18,4 @@ def get_session() -> Generator[Session, None, None]: try: yield session finally: - session.close() + await session.close() diff --git a/backend/src/database/engine.py b/backend/src/database/engine.py index 624332c2b..ba729d6e4 100644 --- a/backend/src/database/engine.py +++ b/backend/src/database/engine.py @@ -1,25 +1,25 @@ # Urlencode the password to pass it to the engine from urllib.parse import quote_plus -from sqlalchemy import create_engine -from sqlalchemy.engine import URL, Engine +from sqlalchemy.engine import URL +from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine, AsyncSession from sqlalchemy.orm import sessionmaker import settings -engine: Engine +engine: AsyncEngine if settings.DB_USE_SQLITE: # Use sqlite database. - engine = create_engine(URL.create( - drivername="sqlite", + engine = create_async_engine(URL.create( + drivername="sqlite+aiosqlite", database="test.db" ), connect_args={"check_same_thread": False}) -else: +else: # pragma: no cover # Use Mariadb database. _encoded_password = quote_plus(settings.DB_PASSWORD) - engine = create_engine(URL.create( - drivername="mariadb+mariadbconnector", + engine = create_async_engine(URL.create( + drivername="mariadb+asyncmy", username=settings.DB_USERNAME, password=_encoded_password, host=settings.DB_HOST, @@ -27,4 +27,5 @@ database=settings.DB_NAME ), pool_pre_ping=True) -DBSession = sessionmaker(autocommit=False, autoflush=False, bind=engine) +# AsyncSession needs expire_on_commit to be False +DBSession = sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession, expire_on_commit=False) diff --git a/backend/src/database/enums.py b/backend/src/database/enums.py index 5af6831a3..fa3c54d5c 100644 --- a/backend/src/database/enums.py +++ b/backend/src/database/enums.py @@ -6,7 +6,7 @@ @enum.unique -class DecisionEnum(enum.Enum): +class DecisionEnum(enum.IntEnum): """Enum for a decision made by a coach or admin""" UNDECIDED = 0 YES = 1 diff --git a/backend/src/database/exceptions.py b/backend/src/database/exceptions.py index 66bc641e5..76949c810 100644 --- a/backend/src/database/exceptions.py +++ b/backend/src/database/exceptions.py @@ -1,4 +1,4 @@ -class PendingMigrationsException(Exception): +class PendingMigrationsException(Exception): # pragma: no cover """ Exception indication the database is not yet fully migrated. """ diff --git a/backend/src/database/models.py b/backend/src/database/models.py index e09fa58e5..2cc28def6 100644 --- a/backend/src/database/models.py +++ b/backend/src/database/models.py @@ -11,6 +11,7 @@ """ from __future__ import annotations +from typing import Optional from uuid import uuid4, UUID from sqlalchemy import Column, Integer, Enum, ForeignKey, Text, Boolean, DateTime, Table, UniqueConstraint @@ -31,7 +32,7 @@ class AuthEmail(Base): email = Column(Text, unique=True, nullable=False) pw_hash = Column(Text, nullable=False) - user: User = relationship("User", back_populates="email_auth", uselist=False) + user: User = relationship("User", back_populates="email_auth", uselist=False, lazy="selectin") class AuthGitHub(Base): @@ -39,10 +40,14 @@ class AuthGitHub(Base): __tablename__ = "github_auths" gh_auth_id = Column(Integer, primary_key=True) - user_id = Column(Integer, ForeignKey("users.user_id"), nullable=False) + + # Allow nullable in case a token gets invalidated + access_token = Column(Text, nullable=True) email = Column(Text, unique=True, nullable=False) + github_user_id = Column(Integer, unique=True, nullable=False) + user_id = Column(Integer, ForeignKey("users.user_id"), nullable=False) - user: User = relationship("User", back_populates="github_auth", uselist=False) + user: User = relationship("User", back_populates="github_auth", uselist=False, lazy="selectin") class AuthGoogle(Base): @@ -53,7 +58,7 @@ class AuthGoogle(Base): user_id = Column(Integer, ForeignKey("users.user_id"), nullable=False) email = Column(Text, unique=True, nullable=False) - user: User = relationship("User", back_populates="google_auth", uselist=False) + user: User = relationship("User", back_populates="google_auth", uselist=False, lazy="selectin") class CoachRequest(Base): @@ -67,8 +72,8 @@ class CoachRequest(Base): user_id = Column(Integer, ForeignKey("users.user_id"), nullable=False) edition_id = Column(Integer, ForeignKey("editions.edition_id"), nullable=False) - edition: Edition = relationship("Edition", back_populates="coach_requests", uselist=False) - user: User = relationship("User", back_populates="coach_request", uselist=False) + edition: Edition = relationship("Edition", back_populates="coach_requests", uselist=False, lazy="selectin") + user: User = relationship("User", back_populates="coach_request", uselist=False, lazy="selectin") class DecisionEmail(Base): @@ -80,7 +85,7 @@ class DecisionEmail(Base): decision = Column(Enum(EmailStatusEnum), nullable=False) date = Column(DateTime, nullable=False) - student: Student = relationship("Student", back_populates="emails", uselist=False) + student: Student = relationship("Student", back_populates="emails", uselist=False, lazy="selectin") class Edition(Base): @@ -88,15 +93,17 @@ class Edition(Base): __tablename__ = "editions" edition_id = Column(Integer, primary_key=True) - name = Column(Text, unique=True, nullable=False) - year = Column(Integer, unique=True, nullable=False) + name: str = Column(Text, unique=True, nullable=False) + year: int = Column(Integer, nullable=False) + readonly: bool = Column(Boolean, nullable=False, default=False) - invite_links: list[InviteLink] = relationship("InviteLink", back_populates="edition") - projects: list[Project] = relationship("Project", back_populates="edition") + invite_links: list[InviteLink] = relationship("InviteLink", back_populates="edition", cascade="all, delete-orphan") + projects: list[Project] = relationship("Project", back_populates="edition", cascade="all, delete-orphan") coaches: list[User] = relationship("User", secondary="user_editions", back_populates="editions") - coach_requests: list[CoachRequest] = relationship("CoachRequest", back_populates="edition") - students: list[Student] = relationship("Student", back_populates="edition") - webhook_urls: list[WebhookURL] = relationship("WebhookURL", back_populates="edition") + coach_requests: list[CoachRequest] = relationship("CoachRequest", back_populates="edition", + cascade="all, delete-orphan") + students: list[Student] = relationship("Student", back_populates="edition", cascade="all, delete-orphan") + webhook_urls: list[WebhookURL] = relationship("WebhookURL", back_populates="edition", cascade="all, delete-orphan") class InviteLink(Base): @@ -108,7 +115,7 @@ class InviteLink(Base): target_email = Column(Text, nullable=False) edition_id = Column(Integer, ForeignKey("editions.edition_id", name="fk_invite_link_edition_id_edition")) - edition: Edition = relationship("Edition", back_populates="invite_links", uselist=False) + edition: Edition = relationship("Edition", back_populates="invite_links", uselist=False, lazy="selectin") class Partner(Base): @@ -118,7 +125,8 @@ class Partner(Base): partner_id = Column(Integer, primary_key=True) name = Column(Text, unique=True, nullable=False) - projects: list[Project] = relationship("Project", secondary="project_partners", back_populates="partners") + projects: list[Project] = relationship("Project", secondary="project_partners", + back_populates="partners", lazy="selectin") class Project(Base): @@ -127,14 +135,16 @@ class Project(Base): project_id = Column(Integer, primary_key=True) name = Column(Text, nullable=False) - number_of_students = Column(Integer, nullable=False, default=0) + info_url = Column(Text, nullable=True) edition_id = Column(Integer, ForeignKey("editions.edition_id")) - edition: Edition = relationship("Edition", back_populates="projects", uselist=False) - coaches: list[User] = relationship("User", secondary="project_coaches", back_populates="projects") - skills: list[Skill] = relationship("Skill", secondary="project_skills", back_populates="projects") - partners: list[Partner] = relationship("Partner", secondary="project_partners", back_populates="projects") - project_roles: list[ProjectRole] = relationship("ProjectRole", back_populates="project") + edition: Edition = relationship("Edition", back_populates="projects", uselist=False, lazy="selectin") + coaches: list[User] = relationship("User", secondary="project_coaches", back_populates="projects", lazy="selectin") + partners: list[Partner] = \ + relationship("Partner", secondary="project_partners", back_populates="projects", lazy="selectin") + project_roles: list[ProjectRole] = \ + relationship("ProjectRole", back_populates="project", lazy="selectin", + cascade="all, delete-orphan") project_coaches = Table( @@ -163,25 +173,41 @@ class ProjectRole(Base): be drafted for multiple projects """ __tablename__ = "project_roles" + project_role_id = Column(Integer, primary_key=True) + + project_id = Column(Integer, ForeignKey("projects.project_id")) + skill_id = Column(Integer, ForeignKey("skills.skill_id")) + description = Column(Text, nullable=True) + slots = Column(Integer, nullable=False, default=0) + + project: Project = relationship("Project", back_populates="project_roles", uselist=False, lazy="selectin") + skill: Skill = relationship("Skill", back_populates="project_roles", uselist=False, lazy="selectin") + suggestions: list[ProjectRoleSuggestion] = relationship("ProjectRoleSuggestion", back_populates="project_role", + lazy="selectin", cascade="all, delete-orphan") - student_id = Column(Integer, ForeignKey("students.student_id"), primary_key=True) - project_id = Column(Integer, ForeignKey("projects.project_id"), primary_key=True) - skill_id = Column(Integer, ForeignKey("skills.skill_id"), primary_key=True) - definitive = Column(Boolean, nullable=False, default=False) + +class ProjectRoleSuggestion(Base): + """Suggestion of a student by a coach for a project role""" + __tablename__ = "pr_suggestions" + __table_args__ = (UniqueConstraint('project_role_id', 'student_id'),) + project_role_suggestion_id = Column(Integer, primary_key=True) argumentation = Column(Text, nullable=True) - drafter_id = Column(Integer, ForeignKey("users.user_id"), nullable=False) - student: Student = relationship("Student", back_populates="project_roles", uselist=False) - project: Project = relationship("Project", back_populates="project_roles", uselist=False) - skill: Skill = relationship("Skill", back_populates="project_roles", uselist=False) - drafter: User = relationship("User", back_populates="drafted_roles", uselist=False) + project_role_id = Column(Integer, ForeignKey("project_roles.project_role_id")) + project_role: ProjectRole = relationship( + "ProjectRole", + back_populates="suggestions", + uselist=False, + lazy="selectin", + cascade="save-update" + ) + student_id = Column(Integer, ForeignKey("students.student_id"), nullable=True) + student: Optional[Student] = \ + relationship("Student", back_populates="pr_suggestions", uselist=False, lazy="selectin") -project_skills = Table( - "project_skills", Base.metadata, - Column("project_id", ForeignKey("projects.project_id")), - Column("skill_id", ForeignKey("skills.skill_id")) -) + drafter_id = Column(Integer, ForeignKey("users.user_id"), nullable=True) + drafter: Optional[User] = relationship("User", back_populates="drafted_roles", uselist=False, lazy="selectin") class Skill(Base): @@ -191,17 +217,15 @@ class Skill(Base): Example: name: Frontend - description: Must know React """ __tablename__ = "skills" skill_id = Column(Integer, primary_key=True) name = Column(Text, nullable=False) - description = Column(Text, nullable=True) - project_roles: list[ProjectRole] = relationship("ProjectRole", back_populates="skill") - projects: list[Project] = relationship("Project", secondary="project_skills", back_populates="skills") - students: list[Student] = relationship("Student", secondary="student_skills", back_populates="skills") + project_roles: list[ProjectRole] = relationship("ProjectRole", back_populates="skill", lazy="selectin") + students: list[Student] = \ + relationship("Student", secondary="student_skills", back_populates="skills", lazy="selectin") class Student(Base): @@ -211,7 +235,7 @@ class Student(Base): student_id = Column(Integer, primary_key=True) first_name = Column(Text, nullable=False) last_name = Column(Text, nullable=False) - preferred_name = Column(Text) + preferred_name = Column(Text, nullable=True) email_address = Column(Text, unique=True, nullable=False) phone_number = Column(Text, unique=True, nullable=True, default=None) alumni = Column(Boolean, nullable=False, default=False) @@ -220,12 +244,16 @@ class Student(Base): wants_to_be_student_coach = Column(Boolean, nullable=False, default=False) edition_id = Column(Integer, ForeignKey("editions.edition_id")) - emails: list[DecisionEmail] = relationship("DecisionEmail", back_populates="student", cascade="all, delete-orphan") - project_roles: list[ProjectRole] = relationship("ProjectRole", back_populates="student") - skills: list[Skill] = relationship("Skill", secondary="student_skills", back_populates="students") - suggestions: list[Suggestion] = relationship("Suggestion", back_populates="student") - questions: list[Question] = relationship("Question", back_populates="student") - edition: Edition = relationship("Edition", back_populates="students", uselist=False) + emails: list[DecisionEmail] = \ + relationship("DecisionEmail", back_populates="student", cascade="all, delete-orphan", lazy="selectin") + pr_suggestions: list[ProjectRoleSuggestion] = \ + relationship("ProjectRoleSuggestion", back_populates="student", lazy="selectin") + skills: list[Skill] = relationship("Skill", secondary="student_skills", back_populates="students", lazy="selectin") + suggestions: list[Suggestion] = relationship("Suggestion", back_populates="student", cascade="all, delete-orphan", + lazy="selectin") + questions: list[Question] = relationship("Question", back_populates="student", lazy="selectin", + cascade="all, delete-orphan") + edition: Edition = relationship("Edition", back_populates="students", uselist=False, lazy="selectin") class Question(Base): @@ -237,9 +265,11 @@ class Question(Base): question = Column(Text, nullable=False) student_id = Column(Integer, ForeignKey("students.student_id"), nullable=False) - answers: list[QuestionAnswer] = relationship("QuestionAnswer", back_populates="question") - files: list[QuestionFileAnswer] = relationship("QuestionFileAnswer", back_populates="question") - student: Student = relationship("Student", back_populates="questions", uselist=False) + answers: list[QuestionAnswer] = relationship("QuestionAnswer", back_populates="question", + cascade="all, delete-orphan", lazy="selectin") + files: list[QuestionFileAnswer] = relationship("QuestionFileAnswer", back_populates="question", + cascade="all, delete-orphan", lazy="selectin") + student: Student = relationship("Student", back_populates="questions", uselist=False, lazy="selectin") class QuestionAnswer(Base): @@ -250,7 +280,7 @@ class QuestionAnswer(Base): answer = Column(Text, nullable=True) question_id = Column(Integer, ForeignKey("questions.question_id"), nullable=False) - question: Question = relationship("Question", back_populates="answers", uselist=False) + question: Question = relationship("Question", back_populates="answers", uselist=False, lazy="selectin") class QuestionFileAnswer(Base): @@ -264,7 +294,7 @@ class QuestionFileAnswer(Base): size = Column(Integer, nullable=False) question_id = Column(Integer, ForeignKey("questions.question_id"), nullable=False) - question: Question = relationship("Question", back_populates="files", uselist=False) + question: Question = relationship("Question", back_populates="files", uselist=False, lazy="selectin") student_skills = Table( @@ -277,18 +307,16 @@ class QuestionFileAnswer(Base): class Suggestion(Base): """A suggestion left by a coach about a student""" __tablename__ = "suggestions" - __table_args__=( - UniqueConstraint('coach_id', 'student_id', name='unique_coach_student_suggestion'), - ) + __table_args__ = (UniqueConstraint('student_id', 'coach_id'),) suggestion_id = Column(Integer, primary_key=True) student_id = Column(Integer, ForeignKey("students.student_id"), nullable=False) - coach_id = Column(Integer, ForeignKey("users.user_id"), nullable=False) + coach_id = Column(Integer, ForeignKey("users.user_id"), nullable=True) suggestion = Column(Enum(DecisionEnum), nullable=False, default=DecisionEnum.UNDECIDED) argumentation = Column(Text, nullable=True) - student: Student = relationship("Student", back_populates="suggestions", uselist=False) - coach: User = relationship("User", back_populates="suggestions", uselist=False) + student: Student = relationship("Student", back_populates="suggestions", uselist=False, lazy="selectin") + coach: Optional[User] = relationship("User", back_populates="suggestions", uselist=False, lazy="selectin") class User(Base): @@ -299,16 +327,24 @@ class User(Base): name = Column(Text, nullable=False) admin = Column(Boolean, nullable=False, default=False) - coach_request: CoachRequest = relationship("CoachRequest", back_populates="user", uselist=False) - drafted_roles: list[ProjectRole] = relationship("ProjectRole", back_populates="drafter") - editions: list[Edition] = relationship("Edition", secondary="user_editions", back_populates="coaches") - projects: list[Project] = relationship("Project", secondary="project_coaches", back_populates="coaches") - suggestions: list[Suggestion] = relationship("Suggestion", back_populates="coach") + coach_request: CoachRequest = relationship("CoachRequest", back_populates="user", uselist=False, + cascade="all, delete-orphan", lazy="selectin") + drafted_roles: list[ProjectRoleSuggestion] = relationship("ProjectRoleSuggestion", back_populates="drafter", + cascade="all, delete-orphan", lazy="selectin") + editions: list[Edition] = relationship("Edition", secondary="user_editions", back_populates="coaches", + lazy="selectin") + projects: list[Project] = relationship("Project", secondary="project_coaches", back_populates="coaches", + lazy="selectin") + suggestions: list[Suggestion] = relationship("Suggestion", back_populates="coach", cascade="all, delete-orphan", + lazy="selectin") # Authentication methods - email_auth: AuthEmail = relationship("AuthEmail", back_populates="user", uselist=False) - github_auth: AuthGitHub = relationship("AuthGitHub", back_populates="user", uselist=False) - google_auth: AuthGoogle = relationship("AuthGoogle", back_populates="user", uselist=False) + email_auth: AuthEmail = relationship("AuthEmail", back_populates="user", uselist=False, + cascade="all, delete-orphan", lazy="selectin") + github_auth: AuthGitHub = relationship("AuthGitHub", back_populates="user", uselist=False, + cascade="all, delete-orphan", lazy="selectin") + google_auth: AuthGoogle = relationship("AuthGoogle", back_populates="user", uselist=False, + cascade="all, delete-orphan", lazy="selectin") user_editions = Table( @@ -326,4 +362,4 @@ class WebhookURL(Base): uuid: UUID = Column(UUIDType(binary=False), default=uuid4) edition_id = Column(Integer, ForeignKey("editions.edition_id")) - edition: Edition = relationship("Edition", back_populates="webhook_urls", uselist=False) + edition: Edition = relationship("Edition", back_populates="webhook_urls", uselist=False, lazy="selectin") diff --git a/backend/templates/invites.txt b/backend/templates/invites.txt new file mode 100644 index 000000000..01972f3cd --- /dev/null +++ b/backend/templates/invites.txt @@ -0,0 +1,11 @@ +Dear future OSOC-coach + +Thank you for your interest in being part of the OSOC-community! With your help and expertise, we can make the students of today become the grand innovators of tomorrow. + +By clicking on the link below you can create an account to access our selections tool: + +{invite_link} + +We hope to see you soon! + +The OSOC-team \ No newline at end of file diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index aa3b62db6..ce956c81b 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,16 +1,17 @@ """Pytest configuration file with fixtures""" -from typing import Generator +from typing import AsyncGenerator, Generator +from unittest.mock import AsyncMock import pytest from alembic import command from alembic import config -from sqlalchemy.orm import Session -from starlette.testclient import TestClient +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession from src.app import app +from src.app.utils.dependencies import get_http_session from src.database.database import get_session from src.database.engine import engine - from tests.utils.authorization import AuthClient @@ -27,54 +28,73 @@ def tables(): @pytest.fixture -def database_session(tables: None) -> Generator[Session, None, None]: +async def database_session(tables) -> AsyncGenerator[AsyncSession, None]: """ Fixture to create a session for every test, and rollback all the transactions so that each tests starts with a clean db """ - connection = engine.connect() - transaction = connection.begin() - session = Session(bind=connection) + connection = await engine.connect() + transaction = await connection.begin() + # AsyncSession needs expire_on_commit to be False + session = AsyncSession(bind=connection, expire_on_commit=False) yield session # Clean up connections & rollback transactions - session.close() + await session.close() # Transactions can be invalidated when an exception is raised # which causes warnings when running the tests # Check if a transaction is still valid before rolling back if transaction.is_valid: - transaction.rollback() + await transaction.rollback() - connection.close() + await connection.close() @pytest.fixture -def test_client(database_session: Session) -> TestClient: +def aiohttp_session() -> AsyncMock: + """Fixture to get a mock aiohttp.ClientSession instance + Used to replace the dependency of the TestClient + """ + yield AsyncMock() + + +@pytest.fixture +def test_client(database_session: AsyncSession, aiohttp_session: AsyncMock) -> AsyncClient: """Fixture to create a testing version of our main application""" - def override_get_session() -> Generator[Session, None, None]: + def override_get_session() -> AsyncGenerator[AsyncSession, None]: """Inner function to override the Session used in the app A session provided by a fixture will be used instead """ yield database_session + def override_get_http_session() -> Generator[AsyncMock, None, None]: + """Inner function to override the ClientSession used in the app""" + yield aiohttp_session + # Replace get_session with a call to this method instead app.dependency_overrides[get_session] = override_get_session - return TestClient(app) + app.dependency_overrides[get_http_session] = override_get_http_session + return AsyncClient(app=app, base_url="http://test") @pytest.fixture -def auth_client(database_session: Session) -> AuthClient: +def auth_client(database_session: AsyncSession, aiohttp_session: AsyncMock) -> AuthClient: """Fixture to get a TestClient that handles authentication""" - def override_get_session() -> Generator[Session, None, None]: + def override_get_session() -> AsyncGenerator[AsyncSession, None]: """Inner function to override the Session used in the app A session provided by a fixture will be used instead """ yield database_session + def override_get_http_session() -> Generator[AsyncMock, None, None]: + """Inner function to override the ClientSession used in the app""" + yield aiohttp_session + # Replace get_session with a call to this method instead app.dependency_overrides[get_session] = override_get_session - return AuthClient(database_session, app) + app.dependency_overrides[get_http_session] = override_get_http_session + return AuthClient(database_session, app=app, base_url="http://test") diff --git a/backend/tests/fill_database.py b/backend/tests/fill_database.py index 930e2577a..d8eab6e95 100644 --- a/backend/tests/fill_database.py +++ b/backend/tests/fill_database.py @@ -1,5 +1,5 @@ from datetime import date -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from src.database.models import (User, AuthEmail, Skill, Student, Edition, CoachRequest, DecisionEmail, InviteLink, Partner, @@ -8,12 +8,12 @@ from src.app.logic.security import get_password_hash -def fill_database(db: Session): +async def fill_database(db: AsyncSession): """A function to fill the database with fake data that can easly be used when testing""" # Editions edition: Edition = Edition(year=2022, name="ed2022") db.add(edition) - db.commit() + await db.commit() # Users admin: User = User(name="admin", admin=True) @@ -24,7 +24,7 @@ def fill_database(db: Session): db.add(coach1) db.add(coach2) db.add(request) - db.commit() + await db.commit() # AuthEmail pw_hash = get_password_hash("wachtwoord") @@ -36,22 +36,22 @@ def fill_database(db: Session): db.add(auth_email_coach1) db.add(auth_email_coach2) db.add(auth_email_request) - db.commit() + await db.commit() # Skill - skill1: Skill = Skill(name="skill1", description="something about skill1") - skill2: Skill = Skill(name="skill2", description="something about skill2") - skill3: Skill = Skill(name="skill3", description="something about skill3") - skill4: Skill = Skill(name="skill4", description="something about skill4") - skill5: Skill = Skill(name="skill5", description="something about skill5") - skill6: Skill = Skill(name="skill6", description="something about skill6") + skill1: Skill = Skill(name="skill1") + skill2: Skill = Skill(name="skill2") + skill3: Skill = Skill(name="skill3") + skill4: Skill = Skill(name="skill4") + skill5: Skill = Skill(name="skill5") + skill6: Skill = Skill(name="skill6") db.add(skill1) db.add(skill2) db.add(skill3) db.add(skill4) db.add(skill5) db.add(skill6) - db.commit() + await db.commit() # Student student01: Student = Student(first_name="Jos", last_name="Vermeulen", preferred_name="Joske", @@ -179,12 +179,12 @@ def fill_database(db: Session): db.add(student29) db.add(student30) db.add(student31) - db.commit() + await db.commit() # CoachRequest coach_request: CoachRequest = CoachRequest(edition=edition, user=request) db.add(coach_request) - db.commit() + await db.commit() # DecisionEmail decision_email1: DecisionEmail = DecisionEmail( @@ -208,7 +208,7 @@ def fill_database(db: Session): db.add(decision_email5) db.add(decision_email6) db.add(decision_email7) - db.commit() + await db.commit() # InviteLink invite_link1: InviteLink = InviteLink( @@ -217,7 +217,7 @@ def fill_database(db: Session): target_email="newuser2@email.com", edition=edition) db.add(invite_link1) db.add(invite_link2) - db.commit() + await db.commit() # Partner partner1: Partner = Partner(name="Partner1") @@ -226,22 +226,22 @@ def fill_database(db: Session): db.add(partner1) db.add(partner2) db.add(partner3) - db.commit() + await db.commit() # Project project1: Project = Project( - name="project1", number_of_students=3, edition=edition, partners=[partner1]) + name="project1", edition=edition, partners=[partner1]) project2: Project = Project( - name="project2", number_of_students=6, edition=edition, partners=[partner2]) + name="project2", edition=edition, partners=[partner2]) project3: Project = Project( - name="project3", number_of_students=2, edition=edition, partners=[partner3]) + name="project3", edition=edition, partners=[partner3]) project4: Project = Project( - name="project4", number_of_students=9, edition=edition, partners=[partner1, partner3]) + name="project4", edition=edition, partners=[partner1, partner3]) db.add(project1) db.add(project2) db.add(project3) db.add(project4) - db.commit() + await db.commit() # Suggestion suggestion1: Suggestion = Suggestion( @@ -268,25 +268,16 @@ def fill_database(db: Session): db.add(suggestion6) db.add(suggestion7) db.add(suggestion8) - db.commit() + await db.commit() # ProjectRole - project_role1: ProjectRole = ProjectRole( - student=student01, project=project1, skill=skill1, drafter=coach1, argumentation="argmunet") - project_role2: ProjectRole = ProjectRole( # This brings a confict - student=student01, project=project2, skill=skill2, drafter=coach2, argumentation="argmunet") - project_role3: ProjectRole = ProjectRole( - student=student09, project=project2, skill=skill3, drafter=coach1, argumentation="argmunet") - project_role3: ProjectRole = ProjectRole( - student=student05, project=project1, skill=skill4, drafter=coach1, argumentation="argmunet") - project_role4: ProjectRole = ProjectRole( - student=student25, project=project3, skill=skill5, drafter=coach1, argumentation="argmunet") - project_role5: ProjectRole = ProjectRole( - student=student29, project=project3, skill=skill6, drafter=coach1, argumentation="argmunet") - project_role6: ProjectRole = ProjectRole( - student=student03, project=project4, skill=skill5, drafter=coach1, argumentation="argmunet") - project_role7: ProjectRole = ProjectRole( - student=student13, project=project4, skill=skill4, drafter=coach1, argumentation="argmunet") + project_role1: ProjectRole = ProjectRole(project=project1, description="description", skill=skill1, slots=1) + project_role2: ProjectRole = ProjectRole(project=project2, description="description", skill=skill2, slots=1) + project_role3: ProjectRole = ProjectRole(project=project1, description="description", skill=skill3, slots=1) + project_role4: ProjectRole = ProjectRole(project=project3, description="description", skill=skill4, slots=1) + project_role5: ProjectRole = ProjectRole(project=project3, description="description", skill=skill3, slots=1) + project_role6: ProjectRole = ProjectRole(project=project4, description="description", skill=skill5, slots=1) + project_role7: ProjectRole = ProjectRole(project=project4, description="description", skill=skill6, slots=1) db.add(project_role1) db.add(project_role2) db.add(project_role3) @@ -294,4 +285,4 @@ def fill_database(db: Session): db.add(project_role5) db.add(project_role6) db.add(project_role7) - db.commit() + await db.commit() diff --git a/backend/tests/test_database/test_crud/test_invites.py b/backend/tests/test_database/test_crud/test_invites.py index 7cead2745..df2a0b9b1 100644 --- a/backend/tests/test_database/test_crud/test_invites.py +++ b/backend/tests/test_database/test_crud/test_invites.py @@ -2,7 +2,7 @@ import pytest import sqlalchemy.exc -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from settings import DB_PAGE_SIZE from src.app.exceptions.parsing import MalformedUUIDError @@ -16,49 +16,49 @@ from src.database.models import Edition, InviteLink -def test_create_invite_link(database_session: Session): +async def test_create_invite_link(database_session: AsyncSession): """Test creation of new invite links""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() # Db empty - assert len(get_pending_invites_for_edition(database_session, edition)) == 0 + assert len(await get_pending_invites_for_edition(database_session, edition)) == 0 # Create new link - create_invite_link(database_session, edition, "test@ema.il") + await create_invite_link(database_session, edition, "test@ema.il") - assert len(get_pending_invites_for_edition(database_session, edition)) == 1 + assert len(await get_pending_invites_for_edition(database_session, edition)) == 1 -def test_delete_invite_link(database_session: Session): +async def test_delete_invite_link(database_session: AsyncSession): """Test deletion of existing invite links""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() # Create new link - new_link = create_invite_link(database_session, edition, "test@ema.il") + new_link = await create_invite_link(database_session, edition, "test@ema.il") - assert len(get_pending_invites_for_edition(database_session, edition)) == 1 - delete_invite_link(database_session, new_link) - assert len(get_pending_invites_for_edition(database_session, edition)) == 0 + assert len(await get_pending_invites_for_edition(database_session, edition)) == 1 + await delete_invite_link(database_session, new_link) + assert len(await get_pending_invites_for_edition(database_session, edition)) == 0 -def test_get_all_pending_invites_empty(database_session: Session): +async def test_get_all_pending_invites_empty(database_session: AsyncSession): """Test fetching all invites for a given edition when db is empty""" edition_one = Edition(year=2022, name="ed2022") edition_two = Edition(year=2023, name="ed2023") database_session.add(edition_one) database_session.add(edition_two) - database_session.commit() + await database_session.commit() # Db empty - assert len(get_pending_invites_for_edition(database_session, edition_one)) == 0 - assert len(get_pending_invites_for_edition(database_session, edition_two)) == 0 + assert len(await get_pending_invites_for_edition(database_session, edition_one)) == 0 + assert len(await get_pending_invites_for_edition(database_session, edition_two)) == 0 -def test_get_all_pending_invites_one_present(database_session: Session): +async def test_get_all_pending_invites_one_present(database_session: AsyncSession): """ Test fetching all invites for two editions when only one of them has valid entries @@ -67,87 +67,87 @@ def test_get_all_pending_invites_one_present(database_session: Session): edition_two = Edition(year=2023, name="ed2023") database_session.add(edition_one) database_session.add(edition_two) - database_session.commit() + await database_session.commit() # Create new link link_one = InviteLink(target_email="test@ema.il", edition=edition_one) database_session.add(link_one) - database_session.commit() + await database_session.commit() - assert len(get_pending_invites_for_edition(database_session, edition_one)) == 1 + assert len(await get_pending_invites_for_edition(database_session, edition_one)) == 1 # Other edition still empty - assert len(get_pending_invites_for_edition(database_session, edition_two)) == 0 + assert len(await get_pending_invites_for_edition(database_session, edition_two)) == 0 -def test_get_all_pending_invites_two_present(database_session: Session): +async def test_get_all_pending_invites_two_present(database_session: AsyncSession): """Test fetching all links for two editions when both of them have data""" edition_one = Edition(year=2022, name="ed2022") edition_two = Edition(year=2023, name="ed2023") database_session.add(edition_one) database_session.add(edition_two) - database_session.commit() + await database_session.commit() # Create new links link_one = InviteLink(target_email="test@ema.il", edition=edition_one) link_two = InviteLink(target_email="test@ema.il", edition=edition_two) database_session.add(link_one) database_session.add(link_two) - database_session.commit() + await database_session.commit() - assert len(get_pending_invites_for_edition(database_session, edition_one)) == 1 - assert len(get_pending_invites_for_edition(database_session, edition_two)) == 1 + assert len(await get_pending_invites_for_edition(database_session, edition_one)) == 1 + assert len(await get_pending_invites_for_edition(database_session, edition_two)) == 1 -def test_get_all_pending_invites_pagination(database_session: Session): +async def test_get_all_pending_invites_pagination(database_session: AsyncSession): """Test fetching all links for two editions when both of them have data""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) for i in range(round(DB_PAGE_SIZE * 1.5)): database_session.add(InviteLink(target_email=f"{i}@example.com", edition=edition)) - database_session.commit() + await database_session.commit() - assert len(get_pending_invites_for_edition_page(database_session, edition, 0)) == DB_PAGE_SIZE - assert len(get_pending_invites_for_edition_page(database_session, edition, 1)) == round( + assert len(await get_pending_invites_for_edition_page(database_session, edition, 0)) == DB_PAGE_SIZE + assert len(await get_pending_invites_for_edition_page(database_session, edition, 1)) == round( DB_PAGE_SIZE * 1.5 ) - DB_PAGE_SIZE -def test_get_invite_link_by_uuid_existing(database_session: Session): +async def test_get_invite_link_by_uuid_existing(database_session: AsyncSession): """Test fetching links by uuid's when it exists""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() debug_uuid = "123e4567-e89b-12d3-a456-426614174000" new_link = InviteLink(target_email="test@ema.il", edition=edition, uuid=UUID(debug_uuid)) database_session.add(new_link) - database_session.commit() + await database_session.commit() - assert get_invite_link_by_uuid(database_session, debug_uuid).invite_link_id == new_link.invite_link_id + assert (await get_invite_link_by_uuid(database_session, debug_uuid)).invite_link_id == new_link.invite_link_id -def test_get_invite_link_by_uuid_non_existing(database_session: Session): +async def test_get_invite_link_by_uuid_non_existing(database_session: AsyncSession): """Test fetching links by uuid's when they don't exist""" # Db empty with pytest.raises(sqlalchemy.exc.NoResultFound): - get_invite_link_by_uuid(database_session, "123e4567-e89b-12d3-a456-426614174011") + await get_invite_link_by_uuid(database_session, "123e4567-e89b-12d3-a456-426614174011") edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() debug_uuid = "123e4567-e89b-12d3-a456-426614174000" new_link = InviteLink(target_email="test@ema.il", edition=edition, uuid=UUID(debug_uuid)) database_session.add(new_link) - database_session.commit() + await database_session.commit() # Non-existent id with pytest.raises(sqlalchemy.exc.NoResultFound): - get_invite_link_by_uuid(database_session, "123e4567-e89b-12d3-a456-426614174011") + await get_invite_link_by_uuid(database_session, "123e4567-e89b-12d3-a456-426614174011") -def test_get_invite_link_by_uuid_malformed(database_session: Session): +async def test_get_invite_link_by_uuid_malformed(database_session: AsyncSession): """Test fetching a link by its uuid when the id is malformed""" with pytest.raises(MalformedUUIDError): - get_invite_link_by_uuid(database_session, "some malformed string that isn't a UUID") + await get_invite_link_by_uuid(database_session, "some malformed string that isn't a UUID") diff --git a/backend/tests/test_database/test_crud/test_partners.py b/backend/tests/test_database/test_crud/test_partners.py new file mode 100644 index 000000000..7491db4ab --- /dev/null +++ b/backend/tests/test_database/test_crud/test_partners.py @@ -0,0 +1,22 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from src.database.crud.partners import create_partner, get_optional_partner_by_name +from src.database.models import Partner + +async def test_create_partner_flush(database_session: AsyncSession): + """test create partner flush""" + partners: list[Partner] = (await database_session.execute(select(Partner))).scalars().all() + assert len(partners) == 0 + await create_partner(database_session, "Ugent", True) + partners: list[Partner] = (await database_session.execute(select(Partner))).scalars().all() + assert len(partners) == 1 + assert partners[0].name == "Ugent" + + +async def test_optional_partner_by_name(database_session: AsyncSession): + """test get partner by name or get none""" + partner: Partner = await get_optional_partner_by_name(database_session, "Ugent") + assert partner is None + await create_partner(database_session, "Ugent", True) + partner: Partner = await get_optional_partner_by_name(database_session, "Ugent") + assert partner.name == "Ugent" diff --git a/backend/tests/test_database/test_crud/test_projects.py b/backend/tests/test_database/test_crud/test_projects.py index c9cd146e8..c06c60b18 100644 --- a/backend/tests/test_database/test_crud/test_projects.py +++ b/backend/tests/test_database/test_crud/test_projects.py @@ -1,229 +1,257 @@ import pytest +from sqlalchemy import select from sqlalchemy.exc import NoResultFound -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from settings import DB_PAGE_SIZE from src.app.schemas.projects import InputProject, QueryParamsProjects import src.database.crud.projects as crud -from src.database.models import Edition, Partner, Project, User, Skill, ProjectRole, Student +from src.database.models import Edition, Partner, Project, User, Skill, ProjectRole, Student, ProjectRoleSuggestion -@pytest.fixture -def database_with_data(database_session: Session) -> Session: - """fixture for adding data to the database""" - edition: Edition = Edition(year=2022, name="ed2022") - database_session.add(edition) - user: User = User(name="coach1") - database_session.add(user) - project1 = Project(name="project1", edition=edition, number_of_students=2) - project2 = Project(name="project2", edition=edition, number_of_students=3) - project3 = Project(name="super nice project", - edition=edition, number_of_students=3, coaches=[user]) - database_session.add(project1) - database_session.add(project2) - database_session.add(project3) - skill1: Skill = Skill(name="skill1", description="something about skill1") - skill2: Skill = Skill(name="skill2", description="something about skill2") - skill3: Skill = Skill(name="skill3", description="something about skill3") - database_session.add(skill1) - database_session.add(skill2) - database_session.add(skill3) - student01: Student = Student(first_name="Jos", last_name="Vermeulen", preferred_name="Joske", - email_address="josvermeulen@mail.com", phone_number="0487/86.24.45", alumni=True, - wants_to_be_student_coach=True, edition=edition, skills=[skill1, skill3]) - student02: Student = Student(first_name="Isabella", last_name="Christensen", preferred_name="Isabella", - email_address="isabella.christensen@example.com", phone_number="98389723", alumni=True, - wants_to_be_student_coach=True, edition=edition, skills=[skill2]) - project_role1: ProjectRole = ProjectRole( - student=student01, project=project1, skill=skill1, drafter=user, argumentation="argmunet") - project_role2: ProjectRole = ProjectRole( - student=student01, project=project2, skill=skill3, drafter=user, argumentation="argmunet") - project_role3: ProjectRole = ProjectRole( - student=student02, project=project1, skill=skill1, drafter=user, argumentation="argmunet") - database_session.add(project_role1) - database_session.add(project_role2) - database_session.add(project_role3) - database_session.commit() - - return database_session - - -@pytest.fixture -def current_edition(database_with_data: Session) -> Edition: - """fixture to get the latest edition""" - return database_with_data.query(Edition).all()[-1] - - -def test_get_all_projects_empty(database_session: Session): +async def test_get_all_projects_empty(database_session: AsyncSession): """test get all projects but there are none""" edition: Edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() - projects: list[Project] = crud.get_projects_for_edition( + await database_session.commit() + projects: list[Project] = await crud.get_projects_for_edition( database_session, edition) assert len(projects) == 0 -def test_get_all_projects(database_with_data: Session, current_edition: Edition): +async def test_get_all_projects(database_session: AsyncSession): """test get all projects""" - projects: list[Project] = crud.get_projects_for_edition( - database_with_data, current_edition) - assert len(projects) == 3 + edition: Edition = Edition(year=2022, name="ed2022") + for i in range(DB_PAGE_SIZE - 1): + database_session.add(Project(name=f"Project {i}", edition=edition)) + await database_session.commit() + projects: list[Project] = await crud.get_projects_for_edition(database_session, edition) + assert len(projects) == DB_PAGE_SIZE - 1 -def test_get_all_projects_pagination(database_session: Session): + +async def test_get_all_projects_pagination(database_session: AsyncSession): """test get all projects paginated""" edition = Edition(year=2022, name="ed2022") - database_session.add(edition) + user: User = User(name="coach") + database_session.add(user) for i in range(round(DB_PAGE_SIZE * 1.5)): - database_session.add( - Project(name=f"Project {i}", edition=edition, number_of_students=5)) - database_session.commit() + database_session.add(Project(name=f"Project {i}", edition=edition)) + await database_session.commit() - assert len(crud.get_projects_for_edition_page(database_session, - edition, QueryParamsProjects(page=0), user=None)) == DB_PAGE_SIZE - assert len(crud.get_projects_for_edition_page(database_session, edition, QueryParamsProjects(page=1), user=None)) == round( - DB_PAGE_SIZE * 1.5 - ) - DB_PAGE_SIZE + assert len( + await crud.get_projects_for_edition_page(database_session, edition, QueryParamsProjects(page=0), user=user) + ) == DB_PAGE_SIZE + assert len( + await crud.get_projects_for_edition_page(database_session, edition, QueryParamsProjects(page=1), user=user) + ) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE -def test_get_project_search_name(database_with_data: Session): +async def test_get_project_search_name(database_session: AsyncSession): """test get project with a specific name""" - edition: Edition = database_with_data.query(Edition).all()[0] - projects: list[Project] = crud.get_projects_for_edition_page( - database_with_data, edition, QueryParamsProjects(name="nice"), user=None) + edition: Edition = Edition(year=2022, name="ed2022") + user: User = User(name="coach") + database_session.add(user) + + for i in range(DB_PAGE_SIZE - 2): + database_session.add(Project(name=f"Project {i}", edition=edition)) + database_session.add(Project(name=f"nice project", edition=edition)) + await database_session.commit() + + projects: list[Project] = await crud.get_projects_for_edition_page( + database_session, edition, QueryParamsProjects(name="nice"), user=user + ) assert len(projects) == 1 - assert projects[0].name == "super nice project" + assert projects[0].name == "nice project" -def test_get_project_search_coach(database_with_data: Session): +async def test_get_project_search_coach(database_session: AsyncSession): """test get projects that you are a coach""" - edition: Edition = database_with_data.query(Edition).all()[0] - user: User = database_with_data.query(User).all()[0] - projects: list[Project] = crud.get_projects_for_edition_page( - database_with_data, edition, QueryParamsProjects(coach=True), user=user) + edition: Edition = Edition(year=2022, name="ed2022") + user: User = User(name="coach") + database_session.add(user) + + for i in range(DB_PAGE_SIZE - 2): + database_session.add(Project(name=f"Project {i}", edition=edition)) + database_session.add(Project(name=f"nice project", edition=edition, coaches=[user])) + await database_session.commit() + + projects: list[Project] = await crud.get_projects_for_edition_page( + database_session, edition, QueryParamsProjects(coach=True), user=user + ) assert len(projects) == 1 -def test_add_project_partner_do_not_exist_yet(database_with_data: Session, current_edition: Edition): +async def test_add_project(database_session: AsyncSession): """tests add a project when the project don't exist yet""" - non_existing_proj: InputProject = InputProject(name="project1", number_of_students=2, skills=[1, 3], - partners=["ugent"], coaches=[1]) - assert len(database_with_data.query(Partner).where( - Partner.name == "ugent").all()) == 0 - new_project: Project = crud.add_project( - database_with_data, current_edition, non_existing_proj) - assert new_project == database_with_data.query(Project).where( - Project.project_id == new_project.project_id).one() - new_partner: Partner = database_with_data.query( - Partner).where(Partner.name == "ugent").one() - - assert new_partner in new_project.partners - assert new_project.name == "project1" - assert new_project.edition == current_edition - assert new_project.number_of_students == 2 - assert len(new_project.coaches) == 1 - assert new_project.coaches[0].user_id == 1 - assert len(new_project.skills) == 2 - assert new_project.skills[0].skill_id == 1 - assert new_project.skills[1].skill_id == 3 - - -def test_add_project_partner_do_exist(database_with_data: Session, current_edition: Edition): - """tests add a project when the project exist already """ - existing_proj: InputProject = InputProject(name="project1", number_of_students=2, skills=[1, 3], - partners=["ugent"], coaches=[1]) - database_with_data.add(Partner(name="ugent")) - assert len(database_with_data.query(Partner).where( - Partner.name == "ugent").all()) == 1 - new_project: Project = crud.add_project( - database_with_data, current_edition, existing_proj) - assert new_project == database_with_data.query(Project).where( - Project.project_id == new_project.project_id).one() - partner: Partner = database_with_data.query( - Partner).where(Partner.name == "ugent").one() - - assert partner in new_project.partners - assert new_project.name == "project1" - assert new_project.edition == current_edition - assert new_project.number_of_students == 2 - assert len(new_project.coaches) == 1 - assert new_project.coaches[0].user_id == 1 - assert len(new_project.skills) == 2 - assert new_project.skills[0].skill_id == 1 - assert new_project.skills[1].skill_id == 3 - - -def test_get_ghost_project(database_with_data: Session): + edition: Edition = Edition(year=2022, name="ed2022") + database_session.add(edition) + await database_session.commit() + + partners: list[Partner] = [Partner(name="partner1"), Partner(name="partner2")] + + input_project: InputProject = InputProject( + name="project 1", + info_url="https://info.com", + partners=["partner1", "partner2"], + coaches=[] + ) + + project: Project = await crud.create_project(database_session, edition, input_project, partners=partners) + + assert len((await database_session.execute(select(Project))).unique().scalars().all()) == 1 + assert project.name == input_project.name + assert project.info_url == input_project.info_url + assert len(project.partners) == len(partners) + assert project.edition == edition + + +async def test_get_project_not_found(database_session: AsyncSession): """test project that don't exist""" with pytest.raises(NoResultFound): - crud.get_project(database_with_data, 500) + await crud.get_project(database_session, 500) -def test_get_project(database_with_data: Session): +async def test_get_project(database_session: AsyncSession): """test get project""" - project: Project = crud.get_project(database_with_data, 1) - assert project.name == "project1" - assert project.number_of_students == 2 + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project 1", edition=edition) + database_session.add(project) + await database_session.commit() + assert project == await crud.get_project(database_session, project.project_id) -def test_delete_project_no_project_roles(database_with_data: Session, current_edition): - """test delete a project that don't has project roles""" - assert len(database_with_data.query(ProjectRole).where( - ProjectRole.project_id == 3).all()) == 0 - assert len(crud.get_projects_for_edition( - database_with_data, current_edition)) == 3 - crud.delete_project(database_with_data, 3) - assert len(crud.get_projects_for_edition( - database_with_data, current_edition)) == 2 - assert 3 not in [project.project_id for project in crud.get_projects_for_edition( - database_with_data, current_edition)] +async def test_delete_project_no_project_roles(database_session: AsyncSession): + """test delete a project that don't have project roles""" + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project 1", edition=edition) + database_session.add(project) + await database_session.commit() + + assert len((await database_session.execute(select(Project))).unique().scalars().all()) == 1 + await crud.delete_project(database_session, project) + assert len((await database_session.execute(select(Project))).unique().scalars().all()) == 0 -def test_delete_project_with_project_roles(database_with_data: Session, current_edition): + +async def test_delete_project_with_project_roles(database_session: AsyncSession): """test delete a project that has project roles""" - assert len(database_with_data.query(ProjectRole).where( - ProjectRole.project_id == 1).all()) > 0 - assert len(crud.get_projects_for_edition( - database_with_data, current_edition)) == 3 - crud.delete_project(database_with_data, 1) - assert len(crud.get_projects_for_edition( - database_with_data, current_edition)) == 2 - assert 1 not in [project.project_id for project in crud.get_projects_for_edition( - database_with_data, current_edition)] - assert len(database_with_data.query(ProjectRole).where( - ProjectRole.project_id == 1).all()) == 0 - - -def test_patch_project(database_with_data: Session, current_edition: Edition): + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project( + name="project 1", + edition=edition, + project_roles=[ProjectRole(slots=1, skill=Skill(name="skill"))] + ) + database_session.add(project) + await database_session.commit() + + assert len((await database_session.execute(select(Project))).unique().scalars().all()) == 1 + assert len((await database_session.execute(select(ProjectRole))).unique().scalars().all()) == 1 + await crud.delete_project(database_session, project) + assert len((await database_session.execute(select(Project))).unique().scalars().all()) == 0 + assert len((await database_session.execute(select(ProjectRole))).unique().scalars().all()) == 0 + + +async def test_delete_project_role(database_session: AsyncSession): + """test delete a project role""" + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project( + name="project 1", + edition=edition, + project_roles=[ProjectRole(slots=1, skill=Skill(name="skill"))] + ) + database_session.add(project) + await database_session.commit() + assert len((await database_session.execute(select(ProjectRole))).unique().scalars().all()) == 1 + await crud.delete_project_role(database_session, 1) + assert len((await database_session.execute(select(ProjectRole))).unique().scalars().all()) == 0 + + +async def test_patch_project(database_session: AsyncSession): """tests patch a project""" - proj: InputProject = InputProject(name="projec1", number_of_students=2, skills=[1, 3], - partners=["ugent"], coaches=[1]) - proj_patched: InputProject = InputProject(name="project1", number_of_students=2, skills=[1, 3], - partners=["ugent"], coaches=[1]) - - assert len(database_with_data.query(Partner).where( - Partner.name == "ugent").all()) == 0 - new_project: Project = crud.add_project( - database_with_data, current_edition, proj) - assert new_project == database_with_data.query(Project).where( - Project.project_id == new_project.project_id).one() - new_partner: Partner = database_with_data.query( - Partner).where(Partner.name == "ugent").one() - crud.patch_project(database_with_data, new_project.project_id, - proj_patched) - - assert new_partner in new_project.partners - assert new_project.name == "project1" - - -def test_get_conflict_students(database_with_data: Session, current_edition: Edition): + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project( + name="project 1", + edition=edition, + coaches=[User(name="coach 1")], + partners=[Partner(name="partner 1")] + ) + database_session.add(project) + await database_session.commit() + + new_user = User(name="coach 2") + new_partner = Partner(name="partner 2") + database_session.add(new_user) + database_session.add(new_partner) + await database_session.commit() + + patch: InputProject = InputProject( + name="project 1 - PATCHED", + partners=[new_partner.name], + coaches=[new_user.user_id] + ) + + await crud.patch_project(database_session, project, patch, [new_partner]) + assert project.name == "project 1 - PATCHED" + assert len(project.partners) == 1 + assert len(project.coaches) == 1 + assert project.partners[0].name == new_partner.name + assert project.coaches[0].user_id == new_user.user_id + + +async def test_get_conflict_students(database_session: AsyncSession): """test if the right ConflictStudent is given""" - conflicts: list[(Student, list[Project])] = crud.get_conflict_students( - database_with_data, current_edition) + edition: Edition = Edition(year=2022, name="ed2022") + skill: Skill = Skill(name="skill 1") + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, + edition=edition + ) + + database_session.add(Student( + first_name="Jos2", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@gmail.com", + phone_number="0487/86.24.46", + alumni=True, + wants_to_be_student_coach=True, + edition=edition + )) + + database_session.add(ProjectRoleSuggestion( + student=student, + project_role=ProjectRole( + skill=skill, + slots=1, + project=Project( + name="project 1", + edition=edition + ) + ) + )) + + database_session.add(ProjectRoleSuggestion( + student=student, + project_role=ProjectRole( + skill=skill, + slots=1, + project=Project( + name="project 2", + edition=edition + ) + ) + )) + await database_session.commit() + + conflicts: list[Student] = await crud.get_conflict_students(database_session, edition) assert len(conflicts) == 1 - assert conflicts[0][0].student_id == 1 - assert len(conflicts[0][1]) == 2 - assert conflicts[0][1][0].project_id == 1 - assert conflicts[0][1][1].project_id == 2 + assert conflicts[0].student_id == student.student_id + assert len(conflicts[0].pr_suggestions) == 2 diff --git a/backend/tests/test_database/test_crud/test_projects_students.py b/backend/tests/test_database/test_crud/test_projects_students.py index 854637a6c..4352685e2 100644 --- a/backend/tests/test_database/test_crud/test_projects_students.py +++ b/backend/tests/test_database/test_crud/test_projects_students.py @@ -1,97 +1,251 @@ import pytest -from sqlalchemy.exc import NoResultFound -from sqlalchemy.orm import Session +from sqlalchemy import select +from sqlalchemy.exc import NoResultFound, IntegrityError +from sqlalchemy.ext.asyncio import AsyncSession +from src.app.schemas.projects import InputArgumentation from src.database.crud.projects_students import ( - remove_student_project, add_student_project, change_project_role) -from src.database.models import Edition, Project, User, Skill, ProjectRole, Student + create_pr_suggestion, + remove_project_role_suggestion, + update_pr_suggestion, + get_pr_suggestion_for_pr_by_student, + get_optional_pr_suggestion_for_pr_by_student, +) +from src.database.models import Edition, Project, User, Skill, ProjectRole, Student, ProjectRoleSuggestion -@pytest.fixture -def database_with_data(database_session: Session) -> Session: - """fixture for adding data to the database""" +async def test_add_pr_suggestion(database_session: AsyncSession): + """tests add student to a project""" + db = database_session edition: Edition = Edition(year=2022, name="ed2022") - database_session.add(edition) - project1 = Project(name="project1", edition=edition, number_of_students=2) - project2 = Project(name="project2", edition=edition, number_of_students=3) - project3 = Project(name="project3", edition=edition, number_of_students=3) - database_session.add(project1) - database_session.add(project2) - database_session.add(project3) + project: Project = Project(name="project1", edition=edition) user: User = User(name="coach1") - database_session.add(user) - skill1: Skill = Skill(name="skill1", description="something about skill1") - skill2: Skill = Skill(name="skill2", description="something about skill2") - skill3: Skill = Skill(name="skill3", description="something about skill3") - database_session.add(skill1) - database_session.add(skill2) - database_session.add(skill3) - student01: Student = Student(first_name="Jos", last_name="Vermeulen", preferred_name="Joske", - email_address="josvermeulen@mail.com", phone_number="0487/86.24.45", alumni=True, - wants_to_be_student_coach=True, edition=edition, skills=[skill1, skill3]) - student02: Student = Student(first_name="Isabella", last_name="Christensen", preferred_name="Isabella", - email_address="isabella.christensen@example.com", phone_number="98389723", alumni=True, - wants_to_be_student_coach=True, edition=edition, skills=[skill2]) - project_role1: ProjectRole = ProjectRole( - student=student01, project=project1, skill=skill1, drafter=user, argumentation="argmunet") - project_role2: ProjectRole = ProjectRole( - student=student01, project=project2, skill=skill3, drafter=user, argumentation="argmunet") - project_role3: ProjectRole = ProjectRole( - student=student02, project=project1, skill=skill1, drafter=user, argumentation="argmunet") - database_session.add(project_role1) - database_session.add(project_role2) - database_session.add(project_role3) - database_session.commit() - - return database_session - - -def test_remove_student_from_project(database_with_data: Session): - """test removing a student form a project""" - assert len(database_with_data.query(ProjectRole).where( - ProjectRole.student_id == 1).all()) == 2 - project: Project = database_with_data.query( - Project).where(Project.project_id == 1).one() - remove_student_project(database_with_data, project, 1) - assert len(database_with_data.query(ProjectRole).where( - ProjectRole.student_id == 1).all()) == 1 + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, edition=edition + ) + skill: Skill = Skill(name="dab") + db.add(user) + db.add(project) + db.add(edition) + db.add(student) + db.add(skill) + await db.commit() + project_role: ProjectRole = ProjectRole(project=project, slots=1, skill=skill) + db.add(project_role) + await db.commit() + + assert len((await db.execute(select(ProjectRoleSuggestion).where(ProjectRoleSuggestion.student == student))).scalars().all()) == 0 + await create_pr_suggestion(db, project_role, student, user, InputArgumentation()) + assert len((await db.execute(select(ProjectRoleSuggestion).where(ProjectRoleSuggestion.student == student))).scalars().all()) == 1 + + +async def test_add_pr_suggestion_duplicate(database_session: AsyncSession): + """tests add student to a project""" + db = database_session + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project1", edition=edition) + user: User = User(name="coach1") + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, edition=edition + ) + skill: Skill = Skill(name="dab") + db.add(user) + db.add(project) + db.add(edition) + db.add(student) + db.add(skill) + await db.commit() + + project_role: ProjectRole = ProjectRole(project=project, slots=1, skill=skill) + db.add(project_role) + await db.commit() + + await create_pr_suggestion(db, project_role, student, user, InputArgumentation()) + with pytest.raises(IntegrityError): + await create_pr_suggestion(db, project_role, student, user, InputArgumentation()) + + +async def test_get_pr_suggestion(database_session: AsyncSession): + """tests add student to a project""" + db = database_session + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project1", edition=edition) + user: User = User(name="coach1") + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, edition=edition + ) + skill: Skill = Skill(name="dab") + db.add(user) + db.add(project) + db.add(edition) + db.add(student) + db.add(skill) + await db.commit() + + project_role: ProjectRole = ProjectRole(project=project, slots=1, skill=skill) + db.add(project_role) + await db.commit() -def test_remove_student_from_project_not_assigned_to(database_with_data: Session): - """test removing a student form a project that don't exist""" - project: Project = database_with_data.query( - Project).where(Project.project_id == 2).one() with pytest.raises(NoResultFound): - remove_student_project(database_with_data, project, 2) + await get_pr_suggestion_for_pr_by_student(db, project_role, student) + await create_pr_suggestion(db, project_role, student, user, InputArgumentation()) + assert (await get_pr_suggestion_for_pr_by_student(db, project_role, student)) is not None -def test_add_student_project(database_with_data: Session): +async def test_get_optional_pr_suggestion(database_session: AsyncSession): """tests add student to a project""" - assert len(database_with_data.query(ProjectRole).where( - ProjectRole.student_id == 2).all()) == 1 - project: Project = database_with_data.query( - Project).where(Project.project_id == 2).one() - add_student_project(database_with_data, project, 2, 2, 1) - assert len(database_with_data.query(ProjectRole).where( - ProjectRole.student_id == 2).all()) == 2 + db = database_session + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project1", edition=edition) + user: User = User(name="coach1") + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, edition=edition + ) + skill: Skill = Skill(name="dab") + db.add(user) + db.add(project) + db.add(edition) + db.add(student) + db.add(skill) + await db.commit() + project_role: ProjectRole = ProjectRole(project=project, slots=1, skill=skill) + db.add(project_role) + await db.commit() + + assert (await get_optional_pr_suggestion_for_pr_by_student(db, project_role, student)) is None + await create_pr_suggestion(db, project_role, student, user, InputArgumentation()) + assert (await get_optional_pr_suggestion_for_pr_by_student(db, project_role, student)) is not None + + +async def test_remove_student_from_project(database_session: AsyncSession): + """test removing a student form a project""" + db = database_session + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project1", edition=edition) + user: User = User(name="coach1") + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, edition=edition + ) + skill: Skill = Skill(name="dab") + db.add(user) + db.add(project) + db.add(edition) + db.add(student) + db.add(skill) + await db.commit() + + project_role: ProjectRole = ProjectRole(project=project, slots=1, skill=skill) + db.add(project_role) + await db.commit() + + await create_pr_suggestion(db, project_role, student, user, InputArgumentation()) + assert len((await db.execute(select(ProjectRoleSuggestion).where(ProjectRoleSuggestion.student == student))).scalars().all()) == 1 + await remove_project_role_suggestion(db, project_role, student) + assert len((await db.execute(select(ProjectRoleSuggestion).where(ProjectRoleSuggestion.student == student))).scalars().all()) == 0 + + +async def test_remove_student_from_project_not_assigned_to(database_session: AsyncSession): + """test removing a student form a project that don't exist""" + db = database_session + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project1", edition=edition) + user: User = User(name="coach1") + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, edition=edition + ) + skill: Skill = Skill(name="dab") + db.add(user) + db.add(project) + db.add(edition) + db.add(student) + db.add(skill) + await db.commit() + + project_role: ProjectRole = ProjectRole(project=project, slots=1, skill=skill) + db.add(project_role) + await db.commit() -def test_change_project_role(database_with_data: Session): - """test change project role""" - assert len(database_with_data.query(ProjectRole).where( - ProjectRole.student_id == 2).all()) == 1 - project: Project = database_with_data.query( - Project).where(Project.project_id == 1).one() - project_role: ProjectRole = database_with_data.query(ProjectRole).where( - ProjectRole.project_id == 1).where(ProjectRole.student_id == 2).one() - assert project_role.skill_id == 1 - change_project_role(database_with_data, project, 2, 2, 1) - assert project_role.skill_id == 2 - - -def test_change_project_role_not_assigned_to(database_with_data: Session): - """test change project role""" - project: Project = database_with_data.query( - Project).where(Project.project_id == 2).one() with pytest.raises(NoResultFound): - change_project_role(database_with_data, project, 2, 2, 1) + await remove_project_role_suggestion(db, project_role, student) + + +async def test_change_project_role(database_session: AsyncSession): + """test change project role""" + db = database_session + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project1", edition=edition) + user: User = User(name="coach1") + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, edition=edition + ) + skill: Skill = Skill(name="dab") + db.add(user) + db.add(project) + db.add(edition) + db.add(student) + db.add(skill) + await db.commit() + + project_role: ProjectRole = ProjectRole(project=project, slots=1, skill=skill) + db.add(project_role) + await db.commit() + + await create_pr_suggestion(db, project_role, student, user, InputArgumentation()) + assert len((await db.execute(select(ProjectRoleSuggestion).where(ProjectRoleSuggestion.student == student))).scalars().all()) == 1 + + updating_user: User = User(name="coach1") + argumentation: InputArgumentation = InputArgumentation(argumentation="+") + + pr_suggestion: ProjectRoleSuggestion = await get_pr_suggestion_for_pr_by_student(db, project_role, student) + await update_pr_suggestion(db, pr_suggestion, updating_user, argumentation) + + assert pr_suggestion.student == student + assert pr_suggestion.drafter == updating_user + assert pr_suggestion.argumentation == "+" + assert pr_suggestion.project_role == project_role + + assert len((await db.execute(select(ProjectRoleSuggestion).where(ProjectRoleSuggestion.student == student))).scalars().all()) == 1 diff --git a/backend/tests/test_database/test_crud/test_register.py b/backend/tests/test_database/test_crud/test_register.py index b4f471b6c..3abb90fc4 100644 --- a/backend/tests/test_database/test_crud/test_register.py +++ b/backend/tests/test_database/test_crud/test_register.py @@ -1,40 +1,54 @@ -from sqlalchemy.orm import Session +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession -from src.database.crud.register import create_user, create_coach_request, create_auth_email -from src.database.models import AuthEmail, CoachRequest, User, Edition +from src.app.schemas.oauth.github import GitHubProfile +from src.database.crud.register import create_user, create_coach_request, create_auth_email, create_auth_github +from src.database.models import AuthEmail, CoachRequest, User, Edition, AuthGitHub -def test_create_user(database_session: Session): +async def test_create_user(database_session: AsyncSession): """Tests for creating a user""" - create_user(database_session, "jos") + await create_user(database_session, "jos") - a = database_session.query(User).where(User.name == "jos").all() + a = (await database_session.execute(select(User).where(User.name == "jos"))).unique().scalars().all() assert len(a) == 1 assert a[0].name == "jos" -def test_react_coach_request(database_session: Session): +async def test_react_coach_request(database_session: AsyncSession): """Tests for creating a coach request""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() - u = create_user(database_session, "jos") - create_coach_request(database_session, u, edition) - a = database_session.query(CoachRequest).where(CoachRequest.user == u).all() + await database_session.commit() + u = await create_user(database_session, "jos") + await create_coach_request(database_session, u, edition) + a = (await database_session.execute(select(CoachRequest).where(CoachRequest.user == u))).unique().scalars().all() assert len(a) == 1 assert a[0].user_id == u.user_id assert u.coach_request == a[0] -def test_create_auth_email(database_session: Session): +async def test_create_auth_email(database_session: AsyncSession): """Tests for creating a auth email""" - u = create_user(database_session, "jos") - create_auth_email(database_session, u, "wachtwoord", "mail@email.com") + u = await create_user(database_session, "jos") + await create_auth_email(database_session, u, "wachtwoord", "mail@email.com") - a = database_session.query(AuthEmail).where(AuthEmail.user == u).all() + a = (await database_session.execute(select(AuthEmail).where(AuthEmail.user == u))).scalars().all() assert len(a) == 1 assert a[0].user_id == u.user_id assert a[0].pw_hash == "wachtwoord" assert u.email_auth == a[0] + + +async def test_create_auth_github(database_session: AsyncSession): + """Test creating a GitHub auth entry""" + user = await create_user(database_session, name="GitHub") + profile = GitHubProfile(access_token="token", email="some@test.email", id=1, name="ghn") + await create_auth_github(database_session, user, profile) + + a = (await database_session.execute(select(AuthGitHub).where(AuthGitHub.user == user))).scalars().all() + + assert len(a) == 1 + assert a[0].user_id == user.user_id diff --git a/backend/tests/test_database/test_crud/test_skills.py b/backend/tests/test_database/test_crud/test_skills.py new file mode 100644 index 000000000..c042914dc --- /dev/null +++ b/backend/tests/test_database/test_crud/test_skills.py @@ -0,0 +1,69 @@ +import pytest +from sqlalchemy import select +from sqlalchemy.exc import NoResultFound +from sqlalchemy.ext.asyncio import AsyncSession + +from src.app.schemas.skills import SkillBase +from src.database.crud import skills as crud +from src.database.models import Skill + + +async def test_get_skill_by_id_exists(database_session: AsyncSession): + """Test getting a skill when it exists""" + skill = Skill(name="skill") + database_session.add(skill) + await database_session.commit() + + found = await crud.get_skill_by_id(database_session, skill.skill_id) + assert found == skill + + +async def test_get_skill_by_id_doesnt_exist(database_session: AsyncSession): + """Test getting a skill when it doesn't exist""" + with pytest.raises(NoResultFound): + await crud.get_skill_by_id(database_session, 1) + + +async def test_create_skill(database_session: AsyncSession): + """Test creating a skill""" + await crud.create_skill(database_session, SkillBase(name="name")) + + skills = (await database_session.execute(select(Skill).where(Skill.name == "name"))).scalars().all() + assert len(skills) == 1 + + +async def test_delete_skill_present(database_session: AsyncSession): + """Test deleting a skill when it exists""" + skill = await crud.create_skill(database_session, SkillBase(name="name")) + await crud.delete_skill(database_session, skill.skill_id) + + skills = (await database_session.execute(select(Skill).where(Skill.name == "name"))).scalars().all() + assert len(skills) == 0 + + +async def test_delete_skill_not_present(database_session: AsyncSession): + """Test deleting a skill when it doesn't exist""" + with pytest.raises(NoResultFound): + await crud.delete_skill(database_session, 1) + + +async def test_create_skill_if_not_present_not_present(database_session: AsyncSession): + """Test conditionally creating a skill when it doesn't exist""" + assert await crud.create_skill_if_not_present(database_session, "name") + skills = (await database_session.execute(select(Skill).where(Skill.name == "name"))).scalars().all() + assert len(skills) == 1 + + +async def test_create_skill_if_not_present_present(database_session: AsyncSession): + """Test conditionally creating a skill when it does exist""" + assert await crud.create_skill_if_not_present(database_session, "name") + assert not await crud.create_skill_if_not_present(database_session, "name") + skills = (await database_session.execute(select(Skill).where(Skill.name == "name"))).scalars().all() + assert len(skills) == 1 + + +async def test_get_skill_by_name(database_session: AsyncSession): + """Test to get skill by name""" + await crud.create_skill_if_not_present(database_session, "name") + skill1: Skill = await crud.get_skill_by_name(database_session, "name") + assert skill1.name == "name" diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index db9001edd..cfcfa2206 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -1,6 +1,7 @@ import datetime import pytest -from sqlalchemy.orm import Session +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm.exc import NoResultFound from src.database.models import Student, User, Edition, Skill, DecisionEmail from src.database.enums import DecisionEnum, EmailStatusEnum @@ -11,12 +12,12 @@ @pytest.fixture -def database_with_data(database_session: Session): +async def database_with_data(database_session: AsyncSession): """A function to fill the database with fake data that can easly be used when testing""" # Editions edition: Edition = Edition(year=2022, name="ed22") database_session.add(edition) - database_session.commit() + await database_session.commit() # Users admin: User = User(name="admin", admin=True) @@ -25,22 +26,22 @@ def database_with_data(database_session: Session): database_session.add(admin) database_session.add(coach1) database_session.add(coach2) - database_session.commit() + await database_session.commit() # Skill - skill1: Skill = Skill(name="skill1", description="something about skill1") - skill2: Skill = Skill(name="skill2", description="something about skill2") - skill3: Skill = Skill(name="skill3", description="something about skill3") - skill4: Skill = Skill(name="skill4", description="important") - skill5: Skill = Skill(name="skill5", description="important") - skill6: Skill = Skill(name="skill6", description="something about skill6") + skill1: Skill = Skill(name="skill1") + skill2: Skill = Skill(name="skill2") + skill3: Skill = Skill(name="skill3") + skill4: Skill = Skill(name="skill4") + skill5: Skill = Skill(name="skill5") + skill6: Skill = Skill(name="skill6") database_session.add(skill1) database_session.add(skill2) database_session.add(skill3) database_session.add(skill4) database_session.add(skill5) database_session.add(skill6) - database_session.commit() + await database_session.commit() # Student student01: Student = Student(first_name="Jos", last_name="Vermeulen", preferred_name="Joske", @@ -52,227 +53,213 @@ def database_with_data(database_session: Session): database_session.add(student01) database_session.add(student30) - database_session.commit() + await database_session.commit() # DecisionEmail decision_email: DecisionEmail = DecisionEmail( student=student01, decision=EmailStatusEnum.APPROVED, date=datetime.datetime.now()) database_session.add(decision_email) - database_session.commit() + await database_session.commit() return database_session -def test_get_student_by_id(database_with_data: Session): +async def test_get_student_by_id(database_with_data: AsyncSession): """Tests if you get the right student""" - student: Student = get_student_by_id(database_with_data, 1) + student: Student = await get_student_by_id(database_with_data, 1) assert student.first_name == "Jos" assert student.last_name == "Vermeulen" assert student.student_id == 1 assert student.email_address == "josvermeulen@mail.com" -def test_no_student(database_with_data: Session): +async def test_no_student(database_with_data: AsyncSession): """Tests if you get an error for a not existing student""" with pytest.raises(NoResultFound): - get_student_by_id(database_with_data, 5) + await get_student_by_id(database_with_data, 5) -def test_definitive_decision_on_student_yes(database_with_data: Session): +async def test_definitive_decision_on_student_yes(database_with_data: AsyncSession): """Tests for definitive decision yes""" - student: Student = get_student_by_id(database_with_data, 1) - set_definitive_decision_on_student( + student: Student = await get_student_by_id(database_with_data, 1) + await set_definitive_decision_on_student( database_with_data, student, DecisionEnum.YES) assert student.decision == DecisionEnum.YES -def test_definitive_decision_on_student_maybe(database_with_data: Session): +async def test_definitive_decision_on_student_maybe(database_with_data: AsyncSession): """Tests for definitive decision maybe""" - student: Student = get_student_by_id(database_with_data, 1) - set_definitive_decision_on_student( + student: Student = await get_student_by_id(database_with_data, 1) + await set_definitive_decision_on_student( database_with_data, student, DecisionEnum.MAYBE) assert student.decision == DecisionEnum.MAYBE -def test_definitive_decision_on_student_no(database_with_data: Session): +async def test_definitive_decision_on_student_no(database_with_data: AsyncSession): """Tests for definitive decision no""" - student: Student = get_student_by_id(database_with_data, 1) - set_definitive_decision_on_student( + student: Student = await get_student_by_id(database_with_data, 1) + await set_definitive_decision_on_student( database_with_data, student, DecisionEnum.NO) assert student.decision == DecisionEnum.NO -def test_delete_student(database_with_data: Session): +async def test_delete_student(database_with_data: AsyncSession): """Tests for deleting a student""" - student: Student = get_student_by_id(database_with_data, 1) - delete_student(database_with_data, student) + student: Student = await get_student_by_id(database_with_data, 1) + await delete_student(database_with_data, student) with pytest.raises(NoResultFound): - get_student_by_id(database_with_data, 1) + await get_student_by_id(database_with_data, 1) -def test_get_all_students(database_with_data: Session): +async def test_get_all_students(database_with_data: AsyncSession): """test get all students""" - edition: Edition = database_with_data.query( - Edition).where(Edition.edition_id == 1).one() - students = get_students(database_with_data, edition, CommonQueryParams()) + edition: Edition = (await database_with_data.execute(select(Edition).where(Edition.edition_id == 1))).scalar_one() + students = await get_students(database_with_data, edition, CommonQueryParams(decisions=[]), None) assert len(students) == 2 -def test_search_students_on_first_name(database_with_data: Session): +async def test_search_students_on_first_name(database_with_data: AsyncSession): """test""" - edition: Edition = database_with_data.query( - Edition).where(Edition.edition_id == 1).one() - students = get_students(database_with_data, edition, - CommonQueryParams(name="Jos")) + edition: Edition = (await database_with_data.execute(select(Edition).where(Edition.edition_id == 1))).scalar_one() + students = await get_students(database_with_data, edition, CommonQueryParams(name="Jos", decisions=[]), None) assert len(students) == 1 -def test_search_students_on_last_name(database_with_data: Session): +async def test_search_students_on_last_name(database_with_data: AsyncSession): """tests search on last name""" - edition: Edition = database_with_data.query( - Edition).where(Edition.edition_id == 1).one() - students = get_students(database_with_data, edition, - CommonQueryParams(name="Vermeulen")) + edition: Edition = (await database_with_data.execute(select(Edition).where(Edition.edition_id == 1))).scalar_one() + students = await get_students(database_with_data, edition, CommonQueryParams(name="Vermeulen", decisions=[]), None) assert len(students) == 1 -def test_search_students_on_between_first_and_last_name(database_with_data: Session): +async def test_search_students_on_between_first_and_last_name(database_with_data: AsyncSession): """tests search on between first- and last name""" - edition: Edition = database_with_data.query( - Edition).where(Edition.edition_id == 1).one() - students = get_students(database_with_data, edition, - CommonQueryParams(name="os V")) + edition: Edition = (await database_with_data.execute(select(Edition).where(Edition.edition_id == 1))).scalar_one() + students = await get_students(database_with_data, edition, CommonQueryParams(name="os V", decisions=[]), None) assert len(students) == 1 -def test_search_students_alumni(database_with_data: Session): +async def test_search_students_alumni(database_with_data: AsyncSession): """tests search on alumni""" - edition: Edition = database_with_data.query( - Edition).where(Edition.edition_id == 1).one() - students = get_students(database_with_data, edition, - CommonQueryParams(alumni=True)) + edition: Edition = (await database_with_data.execute(select(Edition).where(Edition.edition_id == 1))).scalar_one() + students = await get_students(database_with_data, edition, CommonQueryParams(alumni=True, decisions=[]), None) assert len(students) == 1 -def test_search_students_student_coach(database_with_data: Session): +async def test_search_students_student_coach(database_with_data: AsyncSession): """tests search on student coach""" - edition: Edition = database_with_data.query( - Edition).where(Edition.edition_id == 1).one() - students = get_students(database_with_data, edition, - CommonQueryParams(student_coach=True)) + edition: Edition = (await database_with_data.execute(select(Edition).where(Edition.edition_id == 1))).scalar_one() + students = await get_students(database_with_data, edition, CommonQueryParams(student_coach=True, decisions=[]), None) assert len(students) == 1 -def test_search_students_one_skill(database_with_data: Session): +async def test_search_students_one_skill(database_with_data: AsyncSession): """tests search on one skill""" - edition: Edition = database_with_data.query( - Edition).where(Edition.edition_id == 1).one() - skill: Skill = database_with_data.query( - Skill).where(Skill.name == "skill1").one() - students = get_students(database_with_data, edition, - CommonQueryParams(), skills=[skill]) + edition: Edition = (await database_with_data.execute(select(Edition).where(Edition.edition_id == 1))).scalar_one() + skill: Skill = (await database_with_data.execute(select(Skill).where(Skill.name == "skill1"))).scalar_one() + students = await get_students(database_with_data, edition, CommonQueryParams(decisions=[]), None, skills=[skill]) assert len(students) == 1 -def test_search_students_multiple_skills(database_with_data: Session): +async def test_search_students_multiple_skills(database_with_data: AsyncSession): """tests search on multiple skills""" - edition: Edition = database_with_data.query( - Edition).where(Edition.edition_id == 1).one() - skills: list[Skill] = database_with_data.query( - Skill).where(Skill.description == "important").all() - students = get_students(database_with_data, edition, - CommonQueryParams(), skills=skills) + edition: Edition = (await database_with_data.execute(select(Edition).where(Edition.edition_id == 1))).scalar_one() + skills: list[Skill] = [ + (await database_with_data.execute(select(Skill).where(Skill.name == "skill4"))).scalar_one(), + (await database_with_data.execute(select(Skill).where(Skill.name == "skill5"))).scalar_one(), + ] + students = await get_students(database_with_data, edition, CommonQueryParams(decisions=[]), None, skills=skills) assert len(students) == 1 -def test_get_emails(database_with_data: Session): +async def test_get_emails(database_with_data: AsyncSession): """tests to get emails""" - student: Student = get_student_by_id(database_with_data, 1) - emails: list[DecisionEmail] = get_emails(database_with_data, student) + student: Student = await get_student_by_id(database_with_data, 1) + emails: list[DecisionEmail] = await get_emails(database_with_data, student) assert len(emails) == 1 - student = get_student_by_id(database_with_data, 2) - emails: list[DecisionEmail] = get_emails(database_with_data, student) + student = await get_student_by_id(database_with_data, 2) + emails: list[DecisionEmail] = await get_emails(database_with_data, student) assert len(emails) == 0 -def test_create_email_applied(database_with_data: Session): +async def test_create_email_applied(database_with_data: AsyncSession): """test create email applied""" - student: Student = get_student_by_id(database_with_data, 2) - create_email(database_with_data, student, EmailStatusEnum.APPLIED) - emails: list[DecisionEmail] = get_emails(database_with_data, student) + student: Student = await get_student_by_id(database_with_data, 2) + await create_email(database_with_data, student, EmailStatusEnum.APPLIED) + emails: list[DecisionEmail] = await get_emails(database_with_data, student) assert len(emails) == 1 assert emails[0].decision == EmailStatusEnum.APPLIED -def test_create_email_awaiting_project(database_with_data: Session): +async def test_create_email_awaiting_project(database_with_data: AsyncSession): """test create email awaiting project""" - student: Student = get_student_by_id(database_with_data, 2) - create_email(database_with_data, student, EmailStatusEnum.AWAITING_PROJECT) - emails: list[DecisionEmail] = get_emails(database_with_data, student) + student: Student = await get_student_by_id(database_with_data, 2) + await create_email(database_with_data, student, EmailStatusEnum.AWAITING_PROJECT) + emails: list[DecisionEmail] = await get_emails(database_with_data, student) assert len(emails) == 1 assert emails[0].decision == EmailStatusEnum.AWAITING_PROJECT -def test_create_email_approved(database_with_data: Session): +async def test_create_email_approved(database_with_data: AsyncSession): """test create email approved""" - student: Student = get_student_by_id(database_with_data, 2) - create_email(database_with_data, student, EmailStatusEnum.APPROVED) - emails: list[DecisionEmail] = get_emails(database_with_data, student) + student: Student = await get_student_by_id(database_with_data, 2) + await create_email(database_with_data, student, EmailStatusEnum.APPROVED) + emails: list[DecisionEmail] = await get_emails(database_with_data, student) assert len(emails) == 1 assert emails[0].decision == EmailStatusEnum.APPROVED -def test_create_email_contract_confirmed(database_with_data: Session): +async def test_create_email_contract_confirmed(database_with_data: AsyncSession): """test create email contract confirmed""" - student: Student = get_student_by_id(database_with_data, 2) - create_email(database_with_data, student, - EmailStatusEnum.CONTRACT_CONFIRMED) - emails: list[DecisionEmail] = get_emails(database_with_data, student) + student: Student = await get_student_by_id(database_with_data, 2) + await create_email(database_with_data, student, + EmailStatusEnum.CONTRACT_CONFIRMED) + emails: list[DecisionEmail] = await get_emails(database_with_data, student) assert len(emails) == 1 assert emails[0].decision == EmailStatusEnum.CONTRACT_CONFIRMED -def test_create_email_contract_declined(database_with_data: Session): +async def test_create_email_contract_declined(database_with_data: AsyncSession): """test create email contract declined""" - student: Student = get_student_by_id(database_with_data, 2) - create_email(database_with_data, student, - EmailStatusEnum.CONTRACT_DECLINED) - emails: list[DecisionEmail] = get_emails(database_with_data, student) + student: Student = await get_student_by_id(database_with_data, 2) + await create_email(database_with_data, student, + EmailStatusEnum.CONTRACT_DECLINED) + emails: list[DecisionEmail] = await get_emails(database_with_data, student) assert len(emails) == 1 assert emails[0].decision == EmailStatusEnum.CONTRACT_DECLINED -def test_create_email_rejected(database_with_data: Session): +async def test_create_email_rejected(database_with_data: AsyncSession): """test create email rejected""" - student: Student = get_student_by_id(database_with_data, 2) - create_email(database_with_data, student, EmailStatusEnum.REJECTED) - emails: list[DecisionEmail] = get_emails(database_with_data, student) + student: Student = await get_student_by_id(database_with_data, 2) + await create_email(database_with_data, student, EmailStatusEnum.REJECTED) + emails: list[DecisionEmail] = await get_emails(database_with_data, student) assert len(emails) == 1 assert emails[0].decision == EmailStatusEnum.REJECTED -def test_get_last_emails_of_students(database_with_data: Session): +async def test_get_last_emails_of_students(database_with_data: AsyncSession): """tests get last email of all students that got an email""" - student1: Student = get_student_by_id(database_with_data, 1) - student2: Student = get_student_by_id(database_with_data, 2) - edition: Edition = database_with_data.query(Edition).all()[0] - create_email(database_with_data, student1, - EmailStatusEnum.APPLIED) - create_email(database_with_data, student2, - EmailStatusEnum.REJECTED) - create_email(database_with_data, student1, - EmailStatusEnum.CONTRACT_CONFIRMED) + student1: Student = await get_student_by_id(database_with_data, 1) + student2: Student = await get_student_by_id(database_with_data, 2) + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await create_email(database_with_data, student1, + EmailStatusEnum.APPLIED) + await create_email(database_with_data, student2, + EmailStatusEnum.REJECTED) + await create_email(database_with_data, student1, + EmailStatusEnum.CONTRACT_CONFIRMED) edition2: Edition = Edition(year=2023, name="ed2023") database_with_data.add(edition) student: Student = Student(first_name="Mehmet", last_name="Dizdar", preferred_name="Mehmet", email_address="mehmet.dizdar@example.com", phone_number="(787)-938-6216", alumni=True, wants_to_be_student_coach=False, edition=edition2, skills=[]) database_with_data.add(student) - database_with_data.commit() - create_email(database_with_data, student, - EmailStatusEnum.REJECTED) + await database_with_data.commit() + await create_email(database_with_data, student, + EmailStatusEnum.REJECTED) - emails: list[DecisionEmail] = get_last_emails_of_students( + emails: list[DecisionEmail] = await get_last_emails_of_students( database_with_data, edition, EmailsSearchQueryParams(email_status=[])) assert len(emails) == 2 assert emails[0].student_id == 1 @@ -281,16 +268,16 @@ def test_get_last_emails_of_students(database_with_data: Session): assert emails[1].decision == EmailStatusEnum.REJECTED -def test_get_last_emails_of_students_filter_applied(database_with_data: Session): +async def test_get_last_emails_of_students_filter_applied(database_with_data: AsyncSession): """tests get all emails where last emails is applied""" - student1: Student = get_student_by_id(database_with_data, 1) - student2: Student = get_student_by_id(database_with_data, 2) - edition: Edition = database_with_data.query(Edition).all()[0] - create_email(database_with_data, student2, - EmailStatusEnum.APPLIED) - create_email(database_with_data, student1, - EmailStatusEnum.CONTRACT_CONFIRMED) - emails: list[DecisionEmail] = get_last_emails_of_students( + student1: Student = await get_student_by_id(database_with_data, 1) + student2: Student = await get_student_by_id(database_with_data, 2) + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await create_email(database_with_data, student2, + EmailStatusEnum.APPLIED) + await create_email(database_with_data, student1, + EmailStatusEnum.CONTRACT_CONFIRMED) + emails: list[DecisionEmail] = await get_last_emails_of_students( database_with_data, edition, EmailsSearchQueryParams(email_status=[EmailStatusEnum.APPLIED])) assert len(emails) == 1 @@ -298,18 +285,18 @@ def test_get_last_emails_of_students_filter_applied(database_with_data: Session) assert emails[0].decision == EmailStatusEnum.APPLIED -def test_get_last_emails_of_students_filter_awaiting_project(database_with_data: Session): +async def test_get_last_emails_of_students_filter_awaiting_project(database_with_data: AsyncSession): """tests get all emails where last emails is awaiting project""" - student1: Student = get_student_by_id(database_with_data, 1) - student2: Student = get_student_by_id(database_with_data, 2) - edition: Edition = database_with_data.query(Edition).all()[0] - create_email(database_with_data, student1, - EmailStatusEnum.APPLIED) - create_email(database_with_data, student2, - EmailStatusEnum.AWAITING_PROJECT) - create_email(database_with_data, student1, - EmailStatusEnum.CONTRACT_CONFIRMED) - emails: list[DecisionEmail] = get_last_emails_of_students( + student1: Student = await get_student_by_id(database_with_data, 1) + student2: Student = await get_student_by_id(database_with_data, 2) + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await create_email(database_with_data, student1, + EmailStatusEnum.APPLIED) + await create_email(database_with_data, student2, + EmailStatusEnum.AWAITING_PROJECT) + await create_email(database_with_data, student1, + EmailStatusEnum.CONTRACT_CONFIRMED) + emails: list[DecisionEmail] = await get_last_emails_of_students( database_with_data, edition, EmailsSearchQueryParams(email_status=[EmailStatusEnum.AWAITING_PROJECT])) assert len(emails) == 1 @@ -317,18 +304,18 @@ def test_get_last_emails_of_students_filter_awaiting_project(database_with_data: assert emails[0].decision == EmailStatusEnum.AWAITING_PROJECT -def test_get_last_emails_of_students_filter_approved(database_with_data: Session): +async def test_get_last_emails_of_students_filter_approved(database_with_data: AsyncSession): """tests get all emails where last emails is approved""" - student1: Student = get_student_by_id(database_with_data, 1) - student2: Student = get_student_by_id(database_with_data, 2) - edition: Edition = database_with_data.query(Edition).all()[0] - create_email(database_with_data, student1, - EmailStatusEnum.APPLIED) - create_email(database_with_data, student2, - EmailStatusEnum.APPROVED) - create_email(database_with_data, student1, - EmailStatusEnum.CONTRACT_CONFIRMED) - emails: list[DecisionEmail] = get_last_emails_of_students( + student1: Student = await get_student_by_id(database_with_data, 1) + student2: Student = await get_student_by_id(database_with_data, 2) + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await create_email(database_with_data, student1, + EmailStatusEnum.APPLIED) + await create_email(database_with_data, student2, + EmailStatusEnum.APPROVED) + await create_email(database_with_data, student1, + EmailStatusEnum.CONTRACT_CONFIRMED) + emails: list[DecisionEmail] = await get_last_emails_of_students( database_with_data, edition, EmailsSearchQueryParams(email_status=[EmailStatusEnum.APPROVED])) assert len(emails) == 1 @@ -336,16 +323,16 @@ def test_get_last_emails_of_students_filter_approved(database_with_data: Session assert emails[0].decision == EmailStatusEnum.APPROVED -def test_get_last_emails_of_students_filter_contract_confirmed(database_with_data: Session): +async def test_get_last_emails_of_students_filter_contract_confirmed(database_with_data: AsyncSession): """tests get all emails where last emails is contract confirmed""" - student1: Student = get_student_by_id(database_with_data, 1) - student2: Student = get_student_by_id(database_with_data, 2) - edition: Edition = database_with_data.query(Edition).all()[0] - create_email(database_with_data, student1, - EmailStatusEnum.APPLIED) - create_email(database_with_data, student2, - EmailStatusEnum.CONTRACT_CONFIRMED) - emails: list[DecisionEmail] = get_last_emails_of_students( + student1: Student = await get_student_by_id(database_with_data, 1) + student2: Student = await get_student_by_id(database_with_data, 2) + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await create_email(database_with_data, student1, + EmailStatusEnum.APPLIED) + await create_email(database_with_data, student2, + EmailStatusEnum.CONTRACT_CONFIRMED) + emails: list[DecisionEmail] = await get_last_emails_of_students( database_with_data, edition, EmailsSearchQueryParams(email_status=[EmailStatusEnum.CONTRACT_CONFIRMED])) assert len(emails) == 1 @@ -353,18 +340,18 @@ def test_get_last_emails_of_students_filter_contract_confirmed(database_with_dat assert emails[0].decision == EmailStatusEnum.CONTRACT_CONFIRMED -def test_get_last_emails_of_students_filter_contract_declined(database_with_data: Session): +async def test_get_last_emails_of_students_filter_contract_declined(database_with_data: AsyncSession): """tests get all emails where last emails is contract declined""" - student1: Student = get_student_by_id(database_with_data, 1) - student2: Student = get_student_by_id(database_with_data, 2) - edition: Edition = database_with_data.query(Edition).all()[0] - create_email(database_with_data, student1, - EmailStatusEnum.APPLIED) - create_email(database_with_data, student2, - EmailStatusEnum.CONTRACT_DECLINED) - create_email(database_with_data, student1, - EmailStatusEnum.CONTRACT_CONFIRMED) - emails: list[DecisionEmail] = get_last_emails_of_students( + student1: Student = await get_student_by_id(database_with_data, 1) + student2: Student = await get_student_by_id(database_with_data, 2) + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await create_email(database_with_data, student1, + EmailStatusEnum.APPLIED) + await create_email(database_with_data, student2, + EmailStatusEnum.CONTRACT_DECLINED) + await create_email(database_with_data, student1, + EmailStatusEnum.CONTRACT_CONFIRMED) + emails: list[DecisionEmail] = await get_last_emails_of_students( database_with_data, edition, EmailsSearchQueryParams(email_status=[EmailStatusEnum.CONTRACT_DECLINED])) assert len(emails) == 1 @@ -372,18 +359,18 @@ def test_get_last_emails_of_students_filter_contract_declined(database_with_data assert emails[0].decision == EmailStatusEnum.CONTRACT_DECLINED -def test_get_last_emails_of_students_filter_rejected(database_with_data: Session): +async def test_get_last_emails_of_students_filter_rejected(database_with_data: AsyncSession): """tests get all emails where last emails is rejected""" - student1: Student = get_student_by_id(database_with_data, 1) - student2: Student = get_student_by_id(database_with_data, 2) - edition: Edition = database_with_data.query(Edition).all()[0] - create_email(database_with_data, student1, - EmailStatusEnum.APPLIED) - create_email(database_with_data, student2, - EmailStatusEnum.REJECTED) - create_email(database_with_data, student1, - EmailStatusEnum.CONTRACT_CONFIRMED) - emails: list[DecisionEmail] = get_last_emails_of_students( + student1: Student = await get_student_by_id(database_with_data, 1) + student2: Student = await get_student_by_id(database_with_data, 2) + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await create_email(database_with_data, student1, + EmailStatusEnum.APPLIED) + await create_email(database_with_data, student2, + EmailStatusEnum.REJECTED) + await create_email(database_with_data, student1, + EmailStatusEnum.CONTRACT_CONFIRMED) + emails: list[DecisionEmail] = await get_last_emails_of_students( database_with_data, edition, EmailsSearchQueryParams(email_status=[EmailStatusEnum.REJECTED])) assert len(emails) == 1 @@ -391,18 +378,18 @@ def test_get_last_emails_of_students_filter_rejected(database_with_data: Session assert emails[0].decision == EmailStatusEnum.REJECTED -def test_get_last_emails_of_students_first_name(database_with_data: Session): +async def test_get_last_emails_of_students_first_name(database_with_data: AsyncSession): """tests get all emails where last emails is first name""" - student1: Student = get_student_by_id(database_with_data, 1) - student2: Student = get_student_by_id(database_with_data, 2) - edition: Edition = database_with_data.query(Edition).all()[0] - create_email(database_with_data, student1, - EmailStatusEnum.APPLIED) - create_email(database_with_data, student2, - EmailStatusEnum.REJECTED) - create_email(database_with_data, student1, - EmailStatusEnum.CONTRACT_CONFIRMED) - emails: list[DecisionEmail] = get_last_emails_of_students( + student1: Student = await get_student_by_id(database_with_data, 1) + student2: Student = await get_student_by_id(database_with_data, 2) + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await create_email(database_with_data, student1, + EmailStatusEnum.APPLIED) + await create_email(database_with_data, student2, + EmailStatusEnum.REJECTED) + await create_email(database_with_data, student1, + EmailStatusEnum.CONTRACT_CONFIRMED) + emails: list[DecisionEmail] = await get_last_emails_of_students( database_with_data, edition, EmailsSearchQueryParams(name="Jos", email_status=[])) assert len(emails) == 1 @@ -410,18 +397,18 @@ def test_get_last_emails_of_students_first_name(database_with_data: Session): assert emails[0].decision == EmailStatusEnum.CONTRACT_CONFIRMED -def test_get_last_emails_of_students_last_name(database_with_data: Session): +async def test_get_last_emails_of_students_last_name(database_with_data: AsyncSession): """tests get all emails where last emails is last name""" - student1: Student = get_student_by_id(database_with_data, 1) - student2: Student = get_student_by_id(database_with_data, 2) - edition: Edition = database_with_data.query(Edition).all()[0] - create_email(database_with_data, student1, - EmailStatusEnum.APPLIED) - create_email(database_with_data, student2, - EmailStatusEnum.REJECTED) - create_email(database_with_data, student1, - EmailStatusEnum.CONTRACT_CONFIRMED) - emails: list[DecisionEmail] = get_last_emails_of_students( + student1: Student = await get_student_by_id(database_with_data, 1) + student2: Student = await get_student_by_id(database_with_data, 2) + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await create_email(database_with_data, student1, + EmailStatusEnum.APPLIED) + await create_email(database_with_data, student2, + EmailStatusEnum.REJECTED) + await create_email(database_with_data, student1, + EmailStatusEnum.CONTRACT_CONFIRMED) + emails: list[DecisionEmail] = await get_last_emails_of_students( database_with_data, edition, EmailsSearchQueryParams(name="Vermeulen", email_status=[])) assert len(emails) == 1 @@ -429,18 +416,18 @@ def test_get_last_emails_of_students_last_name(database_with_data: Session): assert emails[0].decision == EmailStatusEnum.CONTRACT_CONFIRMED -def test_get_last_emails_of_students_between_first_and_last_name(database_with_data: Session): +async def test_get_last_emails_of_students_between_first_and_last_name(database_with_data: AsyncSession): """tests get all emails where last emails is between first- and last name""" - student1: Student = get_student_by_id(database_with_data, 1) - student2: Student = get_student_by_id(database_with_data, 2) - edition: Edition = database_with_data.query(Edition).all()[0] - create_email(database_with_data, student1, - EmailStatusEnum.APPLIED) - create_email(database_with_data, student2, - EmailStatusEnum.REJECTED) - create_email(database_with_data, student1, - EmailStatusEnum.CONTRACT_CONFIRMED) - emails: list[DecisionEmail] = get_last_emails_of_students( + student1: Student = await get_student_by_id(database_with_data, 1) + student2: Student = await get_student_by_id(database_with_data, 2) + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await create_email(database_with_data, student1, + EmailStatusEnum.APPLIED) + await create_email(database_with_data, student2, + EmailStatusEnum.REJECTED) + await create_email(database_with_data, student1, + EmailStatusEnum.CONTRACT_CONFIRMED) + emails: list[DecisionEmail] = await get_last_emails_of_students( database_with_data, edition, EmailsSearchQueryParams(name="os V", email_status=[])) assert len(emails) == 1 @@ -448,16 +435,16 @@ def test_get_last_emails_of_students_between_first_and_last_name(database_with_d assert emails[0].decision == EmailStatusEnum.CONTRACT_CONFIRMED -def test_get_last_emails_of_students_filter_mutliple_status(database_with_data: Session): +async def test_get_last_emails_of_students_filter_mutliple_status(database_with_data: AsyncSession): """tests get all emails where last emails is applied""" - student1: Student = get_student_by_id(database_with_data, 1) - student2: Student = get_student_by_id(database_with_data, 2) - edition: Edition = database_with_data.query(Edition).all()[0] - create_email(database_with_data, student2, - EmailStatusEnum.APPLIED) - create_email(database_with_data, student1, - EmailStatusEnum.CONTRACT_CONFIRMED) - emails: list[DecisionEmail] = get_last_emails_of_students( + student1: Student = await get_student_by_id(database_with_data, 1) + student2: Student = await get_student_by_id(database_with_data, 2) + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await create_email(database_with_data, student2, + EmailStatusEnum.APPLIED) + await create_email(database_with_data, student1, + EmailStatusEnum.CONTRACT_CONFIRMED) + emails: list[DecisionEmail] = await get_last_emails_of_students( database_with_data, edition, EmailsSearchQueryParams(email_status=[ EmailStatusEnum.APPLIED, EmailStatusEnum.CONTRACT_CONFIRMED diff --git a/backend/tests/test_database/test_crud/test_suggestions.py b/backend/tests/test_database/test_crud/test_suggestions.py index 822663149..4f5104dd0 100644 --- a/backend/tests/test_database/test_crud/test_suggestions.py +++ b/backend/tests/test_database/test_crud/test_suggestions.py @@ -1,5 +1,6 @@ import pytest -from sqlalchemy.orm import Session +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.exc import IntegrityError from sqlalchemy.orm.exc import NoResultFound @@ -13,12 +14,12 @@ @pytest.fixture -def database_with_data(database_session: Session): +async def database_with_data(database_session: AsyncSession): """A function to fill the database with fake data that can easly be used when testing""" # Editions edition: Edition = Edition(year=2022, name="ed22") database_session.add(edition) - database_session.commit() + await database_session.commit() # Users admin: User = User(name="admin", admin=True) @@ -27,22 +28,22 @@ def database_with_data(database_session: Session): database_session.add(admin) database_session.add(coach1) database_session.add(coach2) - database_session.commit() + await database_session.commit() # Skill - skill1: Skill = Skill(name="skill1", description="something about skill1") - skill2: Skill = Skill(name="skill2", description="something about skill2") - skill3: Skill = Skill(name="skill3", description="something about skill3") - skill4: Skill = Skill(name="skill4", description="something about skill4") - skill5: Skill = Skill(name="skill5", description="something about skill5") - skill6: Skill = Skill(name="skill6", description="something about skill6") + skill1: Skill = Skill(name="skill1") + skill2: Skill = Skill(name="skill2") + skill3: Skill = Skill(name="skill3") + skill4: Skill = Skill(name="skill4") + skill5: Skill = Skill(name="skill5") + skill6: Skill = Skill(name="skill6") database_session.add(skill1) database_session.add(skill2) database_session.add(skill3) database_session.add(skill4) database_session.add(skill5) database_session.add(skill6) - database_session.commit() + await database_session.commit() # Student student01: Student = Student(first_name="Jos", last_name="Vermeulen", preferred_name="Joske", @@ -54,29 +55,29 @@ def database_with_data(database_session: Session): database_session.add(student01) database_session.add(student30) - database_session.commit() + await database_session.commit() # Suggestion suggestion1: Suggestion = Suggestion( student=student01, coach=admin, argumentation="Good student", suggestion=DecisionEnum.YES) database_session.add(suggestion1) - database_session.commit() + await database_session.commit() return database_session -def test_create_suggestion_yes(database_with_data: Session): +async def test_create_suggestion_yes(database_with_data: AsyncSession): """Test creat a yes suggestion""" - user: User = database_with_data.query( - User).where(User.name == "coach1").first() - student: Student = database_with_data.query(Student).where( - Student.email_address == "marta.marquez@example.com").first() + user: User = (await database_with_data.execute(select( + User).where(User.name == "coach1"))).unique().scalars().first() + student: Student = (await database_with_data.execute(select(Student).where( + Student.email_address == "marta.marquez@example.com"))).unique().scalars().first() - new_suggestion = create_suggestion( + new_suggestion = await create_suggestion( database_with_data, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") - suggestion: Suggestion = database_with_data.query(Suggestion).where( - Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + suggestion: Suggestion = (await database_with_data.execute(select(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student.student_id))).unique().scalars().one() assert new_suggestion == suggestion @@ -86,19 +87,19 @@ def test_create_suggestion_yes(database_with_data: Session): assert suggestion.argumentation == "This is a good student" -def test_create_suggestion_no(database_with_data: Session): +async def test_create_suggestion_no(database_with_data: AsyncSession): """Test create a no suggestion""" - user: User = database_with_data.query( - User).where(User.name == "coach1").first() - student: Student = database_with_data.query(Student).where( - Student.email_address == "marta.marquez@example.com").first() + user: User = (await database_with_data.execute(select( + User).where(User.name == "coach1"))).unique().scalars().first() + student: Student = (await database_with_data.execute(select(Student).where( + Student.email_address == "marta.marquez@example.com"))).unique().scalars().first() - new_suggestion = create_suggestion( + new_suggestion = await create_suggestion( database_with_data, user.user_id, student.student_id, DecisionEnum.NO, "This is a not good student") - suggestion: Suggestion = database_with_data.query(Suggestion).where( - Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + suggestion: Suggestion = (await database_with_data.execute(select(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student.student_id))).unique().scalars().one() assert new_suggestion == suggestion @@ -108,19 +109,19 @@ def test_create_suggestion_no(database_with_data: Session): assert suggestion.argumentation == "This is a not good student" -def test_create_suggestion_maybe(database_with_data: Session): +async def test_create_suggestion_maybe(database_with_data: AsyncSession): """Test create a maybe suggestion""" - user: User = database_with_data.query( - User).where(User.name == "coach1").first() - student: Student = database_with_data.query(Student).where( - Student.email_address == "marta.marquez@example.com").first() + user: User = (await database_with_data.execute(select( + User).where(User.name == "coach1"))).unique().scalars().first() + student: Student = (await database_with_data.execute(select(Student).where( + Student.email_address == "marta.marquez@example.com"))).unique().scalars().first() - new_suggestion = create_suggestion( + new_suggestion = await create_suggestion( database_with_data, user.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") - suggestion: Suggestion = database_with_data.query(Suggestion).where( - Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + suggestion: Suggestion = (await database_with_data.execute(select(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student.student_id))).unique().scalars().one() assert new_suggestion == suggestion @@ -130,102 +131,102 @@ def test_create_suggestion_maybe(database_with_data: Session): assert suggestion.argumentation == "Idk if it's good student" -def test_get_own_suggestion_existing(database_with_data: Session): +async def test_get_own_suggestion_existing(database_with_data: AsyncSession): """Test getting your own suggestion""" - user: User = database_with_data.query( - User).where(User.name == "coach1").one() - student1: Student = database_with_data.query(Student).where( - Student.email_address == "josvermeulen@mail.com").one() + user: User = (await database_with_data.execute(select( + User).where(User.name == "coach1"))).unique().scalars().one() + student1: Student = (await database_with_data.execute(select(Student).where( + Student.email_address == "josvermeulen@mail.com"))).unique().scalars().one() - suggestion = create_suggestion(database_with_data, user.user_id, student1.student_id, DecisionEnum.YES, "args") + suggestion = await create_suggestion(database_with_data, user.user_id, student1.student_id, DecisionEnum.YES, "args") - assert get_own_suggestion(database_with_data, student1.student_id, user.user_id) == suggestion + assert (await get_own_suggestion(database_with_data, student1.student_id, user.user_id)) == suggestion -def test_get_own_suggestion_non_existing(database_with_data: Session): +async def test_get_own_suggestion_non_existing(database_with_data: AsyncSession): """Test getting your own suggestion when it doesn't exist""" - user: User = database_with_data.query( - User).where(User.name == "coach1").one() - student1: Student = database_with_data.query(Student).where( - Student.email_address == "josvermeulen@mail.com").one() + user: User = (await database_with_data.execute(select( + User).where(User.name == "coach1"))).unique().scalars().one() + student1: Student = (await database_with_data.execute(select(Student).where( + Student.email_address == "josvermeulen@mail.com"))).unique().scalars().one() - assert get_own_suggestion(database_with_data, student1.student_id, user.user_id) is None + assert (await get_own_suggestion(database_with_data, student1.student_id, user.user_id)) is None -def test_get_own_suggestion_fields_none(database_with_data: Session): +async def test_get_own_suggestion_fields_none(database_with_data: AsyncSession): """Test getting your own suggestion when either of the fields are None This is really only to increase coverage, the case isn't possible in practice """ - user: User = database_with_data.query( - User).where(User.name == "coach1").one() - student1: Student = database_with_data.query(Student).where( - Student.email_address == "josvermeulen@mail.com").one() - create_suggestion(database_with_data, user.user_id, student1.student_id, DecisionEnum.YES, "args") + user: User = (await database_with_data.execute(select( + User).where(User.name == "coach1"))).unique().scalars().one() + student1: Student = (await database_with_data.execute(select(Student).where( + Student.email_address == "josvermeulen@mail.com"))).unique().scalars().one() + await create_suggestion(database_with_data, user.user_id, student1.student_id, DecisionEnum.YES, "args") - assert get_own_suggestion(database_with_data, None, user.user_id) is None - assert get_own_suggestion(database_with_data, student1.student_id, None) is None + assert (await get_own_suggestion(database_with_data, None, user.user_id)) is None + assert (await get_own_suggestion(database_with_data, student1.student_id, None)) is None -def test_one_coach_two_students(database_with_data: Session): +async def test_one_coach_two_students(database_with_data: AsyncSession): """Test that one coach can write multiple suggestions""" - user: User = database_with_data.query( - User).where(User.name == "coach1").one() - student1: Student = database_with_data.query(Student).where( - Student.email_address == "marta.marquez@example.com").one() - student2: Student = database_with_data.query(Student).where( - Student.email_address == "josvermeulen@mail.com").one() + user: User = (await database_with_data.execute(select( + User).where(User.name == "coach1"))).unique().scalars().one() + student1: Student = (await database_with_data.execute(select(Student).where( + Student.email_address == "marta.marquez@example.com"))).unique().scalars().one() + student2: Student = (await database_with_data.execute(select(Student).where( + Student.email_address == "josvermeulen@mail.com"))).unique().scalars().one() - create_suggestion(database_with_data, user.user_id, + await create_suggestion(database_with_data, user.user_id, student1.student_id, DecisionEnum.YES, "This is a good student") - create_suggestion(database_with_data, user.user_id, student2.student_id, + await create_suggestion(database_with_data, user.user_id, student2.student_id, DecisionEnum.NO, "This is a not good student") - suggestion1: Suggestion = database_with_data.query(Suggestion).where( - Suggestion.coach == user).where(Suggestion.student_id == student1.student_id).one() + suggestion1: Suggestion = (await database_with_data.execute(select(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student1.student_id))).unique().scalars().one() assert suggestion1.coach == user assert suggestion1.student == student1 assert suggestion1.suggestion == DecisionEnum.YES assert suggestion1.argumentation == "This is a good student" - suggestion2: Suggestion = database_with_data.query(Suggestion).where( - Suggestion.coach == user).where(Suggestion.student_id == student2.student_id).one() + suggestion2: Suggestion = (await database_with_data.execute(select(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student2.student_id))).unique().scalars().one() assert suggestion2.coach == user assert suggestion2.student == student2 assert suggestion2.suggestion == DecisionEnum.NO assert suggestion2.argumentation == "This is a not good student" -def test_multiple_suggestions_about_same_student(database_with_data: Session): +async def test_multiple_suggestions_about_same_student(database_with_data: AsyncSession): """Test get multiple suggestions about the same student""" - user: User = database_with_data.query( - User).where(User.name == "coach1").first() - student: Student = database_with_data.query(Student).where( - Student.email_address == "marta.marquez@example.com").first() + user: User = (await database_with_data.execute(select( + User).where(User.name == "coach1"))).unique().scalars().first() + student: Student = (await database_with_data.execute(select(Student).where( + Student.email_address == "marta.marquez@example.com"))).unique().scalars().first() - create_suggestion(database_with_data, user.user_id, student.student_id, + await create_suggestion(database_with_data, user.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") with pytest.raises(IntegrityError): - create_suggestion(database_with_data, user.user_id, + await create_suggestion(database_with_data, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") -def test_get_suggestions_of_student(database_with_data: Session): +async def test_get_suggestions_of_student(database_with_data: AsyncSession): """Test get all suggestions of a student""" - user1: User = database_with_data.query( - User).where(User.name == "coach1").first() - user2: User = database_with_data.query( - User).where(User.name == "coach2").first() - student: Student = database_with_data.query(Student).where( - Student.email_address == "marta.marquez@example.com").first() + user1: User = (await database_with_data.execute(select( + User).where(User.name == "coach1"))).unique().scalars().first() + user2: User = (await database_with_data.execute(select( + User).where(User.name == "coach2"))).scalars().first() + student: Student = (await database_with_data.execute(select(Student).where( + Student.email_address == "marta.marquez@example.com"))).unique().scalars().first() - create_suggestion(database_with_data, user1.user_id, student.student_id, + await create_suggestion(database_with_data, user1.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") - create_suggestion(database_with_data, user2.user_id, + await create_suggestion(database_with_data, user2.user_id, student.student_id, DecisionEnum.YES, "This is a good student") - suggestions_student = get_suggestions_of_student( + suggestions_student = await get_suggestions_of_student( database_with_data, student.student_id) assert len(suggestions_student) == 2 @@ -233,85 +234,85 @@ def test_get_suggestions_of_student(database_with_data: Session): assert suggestions_student[1].student == student -def test_get_suggestion_by_id(database_with_data: Session): +async def test_get_suggestion_by_id(database_with_data: AsyncSession): """Test get suggestion by id""" - suggestion: Suggestion = get_suggestion_by_id(database_with_data, 1) + suggestion: Suggestion = await get_suggestion_by_id(database_with_data, 1) assert suggestion.student_id == 1 assert suggestion.coach_id == 1 assert suggestion.suggestion == DecisionEnum.YES assert suggestion.argumentation == "Good student" -def test_get_suggestion_by_id_non_existing(database_with_data: Session): +async def test_get_suggestion_by_id_non_existing(database_with_data: AsyncSession): """Test you get an error when you search an id that don't exist""" with pytest.raises(NoResultFound): - get_suggestion_by_id(database_with_data, 900) + await get_suggestion_by_id(database_with_data, 900) -def test_delete_suggestion(database_with_data: Session): +async def test_delete_suggestion(database_with_data: AsyncSession): """Test delete suggestion""" - user: User = database_with_data.query( - User).where(User.name == "coach1").first() - student: Student = database_with_data.query(Student).where( - Student.email_address == "marta.marquez@example.com").first() + user: User = (await database_with_data.execute(select( + User).where(User.name == "coach1"))).unique().scalars().first() + student: Student = (await database_with_data.execute(select(Student).where( + Student.email_address == "marta.marquez@example.com"))).unique().scalars().first() - create_suggestion(database_with_data, user.user_id, + await create_suggestion(database_with_data, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") - suggestion: Suggestion = database_with_data.query(Suggestion).where( - Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + suggestion: Suggestion = (await database_with_data.execute(select(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student.student_id))).unique().scalars().one() - delete_suggestion(database_with_data, suggestion) + await delete_suggestion(database_with_data, suggestion) - suggestions: list[Suggestion] = database_with_data.query(Suggestion).where( - Suggestion.coach == user).where(Suggestion.student_id == student.student_id).all() + suggestions: list[Suggestion] = (await database_with_data.execute(select(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student.student_id))).unique().scalars().all() assert len(suggestions) == 0 -def test_update_suggestion(database_with_data: Session): +async def test_update_suggestion(database_with_data: AsyncSession): """Test update suggestion""" - user: User = database_with_data.query( - User).where(User.name == "coach1").first() - student: Student = database_with_data.query(Student).where( - Student.email_address == "marta.marquez@example.com").first() + user: User = (await database_with_data.execute(select( + User).where(User.name == "coach1"))).unique().scalars().first() + student: Student = (await database_with_data.execute(select(Student).where( + Student.email_address == "marta.marquez@example.com"))).unique().scalars().first() - create_suggestion(database_with_data, user.user_id, + await create_suggestion(database_with_data, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") - suggestion: Suggestion = database_with_data.query(Suggestion).where( - Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + suggestion: Suggestion = (await database_with_data.execute(select(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student.student_id))).unique().scalars().one() - update_suggestion(database_with_data, suggestion, + await update_suggestion(database_with_data, suggestion, DecisionEnum.NO, "Not that good student") - new_suggestion: Suggestion = database_with_data.query(Suggestion).where( - Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + new_suggestion: Suggestion = (await database_with_data.execute(select(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student.student_id))).unique().scalars().one() assert new_suggestion.suggestion == DecisionEnum.NO assert new_suggestion.argumentation == "Not that good student" -def test_get_suggestions_of_student_by_type(database_with_data: Session): +async def test_get_suggestions_of_student_by_type(database_with_data: AsyncSession): """Tests get suggestion of a student by type of suggestion""" - user1: User = database_with_data.query( - User).where(User.name == "coach1").first() - user2: User = database_with_data.query( - User).where(User.name == "coach2").first() - user3: User = database_with_data.query( - User).where(User.name == "admin").first() - student: Student = database_with_data.query(Student).where( - Student.email_address == "marta.marquez@example.com").first() - - create_suggestion(database_with_data, user1.user_id, student.student_id, + user1: User = (await database_with_data.execute(select( + User).where(User.name == "coach1"))).unique().scalars().first() + user2: User = (await database_with_data.execute(select( + User).where(User.name == "coach2"))).scalars().first() + user3: User = (await database_with_data.execute(select( + User).where(User.name == "admin"))).scalars().first() + student: Student = (await database_with_data.execute(select(Student).where( + Student.email_address == "marta.marquez@example.com"))).unique().scalars().first() + + await create_suggestion(database_with_data, user1.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") - create_suggestion(database_with_data, user2.user_id, + await create_suggestion(database_with_data, user2.user_id, student.student_id, DecisionEnum.YES, "This is a good student") - create_suggestion(database_with_data, user3.user_id, + await create_suggestion(database_with_data, user3.user_id, student.student_id, DecisionEnum.NO, "This is not a good student") - suggestions_student_yes = get_suggestions_of_student_by_type( + suggestions_student_yes = await get_suggestions_of_student_by_type( database_with_data, student.student_id, DecisionEnum.YES) - suggestions_student_no = get_suggestions_of_student_by_type( + suggestions_student_no = await get_suggestions_of_student_by_type( database_with_data, student.student_id, DecisionEnum.NO) - suggestions_student_maybe = get_suggestions_of_student_by_type( + suggestions_student_maybe = await get_suggestions_of_student_by_type( database_with_data, student.student_id, DecisionEnum.MAYBE) assert len(suggestions_student_yes) == 1 assert len(suggestions_student_no) == 1 diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index df2ce9887..c3a336dfc 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -1,5 +1,6 @@ import pytest -from sqlalchemy.orm import Session +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession import src.database.crud.users as users_crud from settings import DB_PAGE_SIZE @@ -9,7 +10,7 @@ @pytest.fixture -def data(database_session: Session) -> dict[str, str]: +async def data(database_session: AsyncSession) -> dict[str, str]: """Fill database with dummy data""" # Create users @@ -24,16 +25,16 @@ def data(database_session: Session) -> dict[str, str]: edition2 = models.Edition(year=2, name="ed2") database_session.add(edition2) - database_session.commit() + await database_session.commit() email_auth1 = models.AuthEmail(user_id=user1.user_id, email="user1@mail.com", pw_hash="HASH1") - github_auth1 = models.AuthGitHub(user_id=user2.user_id, gh_auth_id=123, email="user2@mail.com") + github_auth1 = models.AuthGitHub(user_id=user2.user_id, gh_auth_id=123, email="user2@mail.com", github_user_id=2) database_session.add(email_auth1) database_session.add(github_auth1) - database_session.commit() + await database_session.commit() # Create coach roles - database_session.execute(models.user_editions.insert(), [ + await database_session.execute(models.user_editions.insert(), [ {"user_id": user1.user_id, "edition_id": edition1.edition_id}, {"user_id": user2.user_id, "edition_id": edition1.edition_id}, {"user_id": user2.user_id, "edition_id": edition2.edition_id} @@ -47,207 +48,216 @@ def data(database_session: Session) -> dict[str, str]: } -def test_get_all_users(database_session: Session, data: dict[str, int]): +async def test_get_all_users(database_session: AsyncSession, data: dict[str, int]): """Test get request for users""" # get all users - users = users_crud.get_users_filtered_page(database_session, FilterParameters()) + users = await users_crud.get_users_filtered_page(database_session, FilterParameters()) assert len(users) == 2, "Wrong length" user_ids = [user.user_id for user in users] assert data["user1"] in user_ids assert data["user2"] in user_ids -def test_get_all_users_paginated(database_session: Session): +async def test_get_all_users_paginated(database_session: AsyncSession): for i in range(round(DB_PAGE_SIZE * 1.5)): database_session.add(models.User(name=f"User {i}", admin=False)) - database_session.commit() + await database_session.commit() - assert len(users_crud.get_users_filtered_page(database_session, FilterParameters(page=0))) == DB_PAGE_SIZE - assert len(users_crud.get_users_filtered_page(database_session, FilterParameters(page=1))) == round( + assert len(await users_crud.get_users_filtered_page(database_session, FilterParameters(page=0))) == DB_PAGE_SIZE + assert len(await users_crud.get_users_filtered_page(database_session, FilterParameters(page=1))) == round( DB_PAGE_SIZE * 1.5 ) - DB_PAGE_SIZE -def test_get_all_users_paginated_filter_name(database_session: Session): +async def test_get_all_users_paginated_filter_name(database_session: AsyncSession): count = 0 for i in range(round(DB_PAGE_SIZE * 1.5)): database_session.add(models.User(name=f"User {i}", admin=False)) if "1" in str(i): count += 1 - database_session.commit() + await database_session.commit() - assert len(users_crud.get_users_filtered_page(database_session, FilterParameters(page=0, name="1"))) == count - assert len(users_crud.get_users_filtered_page(database_session, FilterParameters(page=1, name="1"))) == max( + assert len(await users_crud.get_users_filtered_page(database_session, FilterParameters(page=0, name="1"))) == count + assert len(await users_crud.get_users_filtered_page(database_session, FilterParameters(page=1, name="1"))) == max( count - round( DB_PAGE_SIZE * 1.5), 0) -def test_get_all_admins(database_session: Session, data: dict[str, str]): +async def test_get_all_admins(database_session: AsyncSession, data: dict[str, str]): """Test get request for admins""" # get all admins - users = users_crud.get_users_filtered_page(database_session, FilterParameters(admin=True)) + users = await users_crud.get_users_filtered_page(database_session, FilterParameters(admin=True)) assert len(users) == 1, "Wrong length" assert data["user1"] == users[0].user_id -def test_get_all_admins_paginated(database_session: Session): +async def test_get_all_admins_paginated(database_session: AsyncSession): admins = [] for i in range(round(DB_PAGE_SIZE * 3)): user = models.User(name=f"User {i}", admin=i % 2 == 0) database_session.add(user) if i % 2 == 0: admins.append(user) - database_session.commit() + await database_session.commit() count = len(admins) - users = users_crud.get_users_filtered_page(database_session, FilterParameters(page=0, admin=True)) + users = await users_crud.get_users_filtered_page(database_session, FilterParameters(page=0, admin=True)) assert len(users) == min(count, DB_PAGE_SIZE) for user in users: assert user in admins - assert len(users_crud.get_users_filtered_page(database_session, FilterParameters(page=1, admin=True))) == \ + assert len(await users_crud.get_users_filtered_page(database_session, FilterParameters(page=1, admin=True))) == \ min(count - DB_PAGE_SIZE, DB_PAGE_SIZE) -def test_get_all_non_admins_paginated(database_session: Session): +async def test_get_all_non_admins_paginated(database_session: AsyncSession): non_admins = [] for i in range(round(DB_PAGE_SIZE * 3)): user = models.User(name=f"User {i}", admin=i % 2 == 0) database_session.add(user) if i % 2 != 0: non_admins.append(user) - database_session.commit() + await database_session.commit() count = len(non_admins) - users = users_crud.get_users_filtered_page(database_session, FilterParameters(page=0, admin=False)) + users = await users_crud.get_users_filtered_page(database_session, FilterParameters(page=0, admin=False)) assert len(users) == min(count, DB_PAGE_SIZE) for user in users: assert user in non_admins - assert len(users_crud.get_users_filtered_page(database_session, FilterParameters(page=1, admin=False))) == \ + assert len(await users_crud.get_users_filtered_page(database_session, FilterParameters(page=1, admin=False))) == \ min(count - DB_PAGE_SIZE, DB_PAGE_SIZE) -def test_get_all_admins_paginated_filter_name(database_session: Session): +async def test_get_all_admins_paginated_filter_name(database_session: AsyncSession): count = 0 for i in range(round(DB_PAGE_SIZE * 1.5)): database_session.add(models.User(name=f"User {i}", admin=i % 2 == 0)) if "1" in str(i) and i % 2 == 0: count += 1 - database_session.commit() + await database_session.commit() assert len( - users_crud.get_users_filtered_page(database_session, FilterParameters(page=0, name="1", admin=True))) == count + await users_crud.get_users_filtered_page(database_session, + FilterParameters(page=0, name="1", admin=True))) == count assert len( - users_crud.get_users_filtered_page(database_session, FilterParameters(page=1, name="1", admin=True))) == max( + await users_crud.get_users_filtered_page(database_session, + FilterParameters(page=1, name="1", admin=True))) == max( count - round( DB_PAGE_SIZE * 1.5), 0) -def test_get_user_edition_names_empty(database_session: Session): +async def test_get_user_editions_empty(database_session: AsyncSession): """Test getting all editions from a user when there are none""" user = models.User(name="test") database_session.add(user) - database_session.commit() + await database_session.commit() + # query the user to initiate association tables + await database_session.execute(select(models.User).where(models.User.user_id == user.user_id)) # No editions yet - editions = users_crud.get_user_edition_names(database_session, user) + editions = await users_crud.get_user_editions(database_session, user) assert len(editions) == 0 -def test_get_user_edition_names_admin(database_session: Session): +async def test_get_user_editions_admin(database_session: AsyncSession): """Test getting all editions for an admin""" user = models.User(name="test", admin=True) database_session.add(user) edition = models.Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() + + # query the user to initiate association tables + await database_session.execute(select(models.User).where(models.User.user_id == user.user_id)) # Not added to edition yet, but admin can see it anyway - editions = users_crud.get_user_edition_names(database_session, user) + editions = await users_crud.get_user_editions(database_session, user) assert len(editions) == 1 -def test_get_user_edition_names_coach(database_session: Session): +async def test_get_user_editions_coach(database_session: AsyncSession): """Test getting all editions for a coach when they aren't empty""" user = models.User(name="test") database_session.add(user) edition = models.Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() + + # query the user to initiate association tables + await database_session.execute(select(models.User).where(models.User.user_id == user.user_id)) # No editions yet - editions = users_crud.get_user_edition_names(database_session, user) + editions = await users_crud.get_user_editions(database_session, user) assert len(editions) == 0 # Add user to a new edition user.editions.append(edition) database_session.add(user) - database_session.commit() + await database_session.commit() - # No editions yet - editions = users_crud.get_user_edition_names(database_session, user) - assert editions == [edition.name] + editions = await users_crud.get_user_editions(database_session, user) + assert editions[0].name == edition.name -def test_get_all_users_from_edition(database_session: Session, data: dict[str, str]): +async def test_get_all_users_from_edition(database_session: AsyncSession, data: dict[str, str]): """Test get request for users of a given edition""" # get all users from edition - users = users_crud.get_users_filtered_page(database_session, FilterParameters(edition=data["edition1"])) + users = await users_crud.get_users_filtered_page(database_session, FilterParameters(edition=data["edition1"])) assert len(users) == 2, "Wrong length" user_ids = [user.user_id for user in users] assert data["user1"] in user_ids assert data["user2"] in user_ids - users = users_crud.get_users_filtered_page(database_session, FilterParameters(edition=data["edition2"])) + users = await users_crud.get_users_filtered_page(database_session, FilterParameters(edition=data["edition2"])) assert len(users) == 1, "Wrong length" assert data["user2"] == users[0].user_id -def test_get_all_users_for_edition_paginated(database_session: Session): +async def test_get_all_users_for_edition_paginated(database_session: AsyncSession): edition_1 = models.Edition(year=2022, name="ed2022") edition_2 = models.Edition(year=2023, name="ed2023") database_session.add(edition_1) database_session.add(edition_2) - database_session.commit() + await database_session.commit() for i in range(round(DB_PAGE_SIZE * 1.5)): user_1 = models.User(name=f"User {i} - a", admin=False) user_2 = models.User(name=f"User {i} - b", admin=False) database_session.add(user_1) database_session.add(user_2) - database_session.commit() - database_session.execute(models.user_editions.insert(), [ + await database_session.commit() + await database_session.execute(models.user_editions.insert(), [ {"user_id": user_1.user_id, "edition_id": edition_1.edition_id}, {"user_id": user_2.user_id, "edition_id": edition_2.edition_id}, ]) - database_session.commit() + await database_session.commit() - assert len(users_crud.get_users_filtered_page(database_session, FilterParameters(edition=edition_1.name, - page=0))) == DB_PAGE_SIZE - assert len(users_crud.get_users_filtered_page(database_session, - FilterParameters(edition=edition_1.name, page=1))) == round( + assert len(await users_crud.get_users_filtered_page(database_session, FilterParameters(edition=edition_1.name, + page=0))) == DB_PAGE_SIZE + assert len(await users_crud.get_users_filtered_page(database_session, + FilterParameters(edition=edition_1.name, page=1))) == round( DB_PAGE_SIZE * 1.5 ) - DB_PAGE_SIZE - assert len(users_crud.get_users_filtered_page(database_session, FilterParameters(edition=edition_2.name, - page=0))) == DB_PAGE_SIZE - assert len(users_crud.get_users_filtered_page(database_session, - FilterParameters(edition=edition_2.name, page=1))) == round( + assert len(await users_crud.get_users_filtered_page(database_session, FilterParameters(edition=edition_2.name, + page=0))) == DB_PAGE_SIZE + assert len(await users_crud.get_users_filtered_page(database_session, + FilterParameters(edition=edition_2.name, page=1))) == round( DB_PAGE_SIZE * 1.5 ) - DB_PAGE_SIZE -def test_get_all_users_for_edition_paginated_filter_name(database_session: Session): +async def test_get_all_users_for_edition_paginated_filter_name(database_session: AsyncSession): edition_1 = models.Edition(year=2022, name="ed2022") edition_2 = models.Edition(year=2023, name="ed2023") database_session.add(edition_1) database_session.add(edition_2) - database_session.commit() + await database_session.commit() count = 0 for i in range(round(DB_PAGE_SIZE * 1.5)): @@ -255,73 +265,73 @@ def test_get_all_users_for_edition_paginated_filter_name(database_session: Sessi user_2 = models.User(name=f"User {i} - b", admin=False) database_session.add(user_1) database_session.add(user_2) - database_session.commit() - database_session.execute(models.user_editions.insert(), [ + await database_session.commit() + await database_session.execute(models.user_editions.insert(), [ {"user_id": user_1.user_id, "edition_id": edition_1.edition_id}, {"user_id": user_2.user_id, "edition_id": edition_2.edition_id}, ]) if "1" in str(i): count += 1 - database_session.commit() + await database_session.commit() - assert len(users_crud.get_users_filtered_page(database_session, - FilterParameters(edition=edition_1.name, page=0, name="1"))) == \ + assert len(await users_crud.get_users_filtered_page(database_session, + FilterParameters(edition=edition_1.name, page=0, name="1"))) == \ min(count, DB_PAGE_SIZE) - assert len(users_crud.get_users_filtered_page(database_session, - FilterParameters(edition=edition_1.name, page=1, name="1"))) == \ + assert len(await users_crud.get_users_filtered_page(database_session, + FilterParameters(edition=edition_1.name, page=1, name="1"))) == \ max(count - DB_PAGE_SIZE, 0) - assert len(users_crud.get_users_filtered_page(database_session, - FilterParameters(edition=edition_2.name, page=0, name="1"))) == \ + assert len(await users_crud.get_users_filtered_page(database_session, + FilterParameters(edition=edition_2.name, page=0, name="1"))) == \ min(count, DB_PAGE_SIZE) - assert len(users_crud.get_users_filtered_page(database_session, - FilterParameters(edition=edition_2.name, page=1, name="1"))) == \ + assert len(await users_crud.get_users_filtered_page(database_session, + FilterParameters(edition=edition_2.name, page=1, name="1"))) == \ max(count - DB_PAGE_SIZE, 0) -def test_get_all_users_excluded_edition_paginated(database_session: Session): +async def test_get_all_users_excluded_edition_paginated(database_session: AsyncSession): edition_a = models.Edition(year=2022, name="edA") edition_b = models.Edition(year=2023, name="edB") database_session.add(edition_a) database_session.add(edition_b) - database_session.commit() + await database_session.commit() for i in range(round(DB_PAGE_SIZE * 1.5)): user_1 = models.User(name=f"User {i} - a", admin=False) user_2 = models.User(name=f"User {i} - b", admin=False) database_session.add(user_1) database_session.add(user_2) - database_session.commit() - database_session.execute(models.user_editions.insert(), [ + await database_session.commit() + await database_session.execute(models.user_editions.insert(), [ {"user_id": user_1.user_id, "edition_id": edition_a.edition_id}, {"user_id": user_2.user_id, "edition_id": edition_b.edition_id}, ]) - database_session.commit() + await database_session.commit() - a_users = users_crud.get_users_filtered_page(database_session, - FilterParameters(page=0, exclude_edition="edB", name="")) + a_users = await users_crud.get_users_filtered_page(database_session, + FilterParameters(page=0, exclude_edition="edB", name="")) assert len(a_users) == DB_PAGE_SIZE for user in a_users: assert "b" not in user.name - assert len(users_crud.get_users_filtered_page(database_session, - FilterParameters(page=1, exclude_edition="edB", name=""))) == \ + assert len(await users_crud.get_users_filtered_page(database_session, + FilterParameters(page=1, exclude_edition="edB", name=""))) == \ round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE - b_users = users_crud.get_users_filtered_page(database_session, - FilterParameters(page=0, exclude_edition="edA", name="")) + b_users = await users_crud.get_users_filtered_page(database_session, + FilterParameters(page=0, exclude_edition="edA", name="")) assert len(b_users) == DB_PAGE_SIZE for user in b_users: assert "a" not in user.name - assert len(users_crud.get_users_filtered_page(database_session, - FilterParameters(page=1, exclude_edition="edA", name=""))) == \ + assert len(await users_crud.get_users_filtered_page(database_session, + FilterParameters(page=1, exclude_edition="edA", name=""))) == \ round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE -def test_get_all_users_excluded_edition_paginated_filter_name(database_session: Session): +async def test_get_all_users_excluded_edition_paginated_filter_name(database_session: AsyncSession): edition_a = models.Edition(year=2022, name="edA") edition_b = models.Edition(year=2023, name="edB") database_session.add(edition_a) database_session.add(edition_b) - database_session.commit() + await database_session.commit() count = 0 for i in range(round(DB_PAGE_SIZE * 1.5)): @@ -329,40 +339,40 @@ def test_get_all_users_excluded_edition_paginated_filter_name(database_session: user_2 = models.User(name=f"User {i} - b", admin=False) database_session.add(user_1) database_session.add(user_2) - database_session.commit() - database_session.execute(models.user_editions.insert(), [ + await database_session.commit() + await database_session.execute(models.user_editions.insert(), [ {"user_id": user_1.user_id, "edition_id": edition_a.edition_id}, {"user_id": user_2.user_id, "edition_id": edition_b.edition_id}, ]) if "1" in str(i): count += 1 - database_session.commit() + await database_session.commit() - a_users = users_crud.get_users_filtered_page(database_session, - FilterParameters(page=0, exclude_edition="edB", name="1")) + a_users = await users_crud.get_users_filtered_page(database_session, + FilterParameters(page=0, exclude_edition="edB", name="1")) assert len(a_users) == min(count, DB_PAGE_SIZE) for user in a_users: assert "b" not in user.name - assert len(users_crud.get_users_filtered_page(database_session, - FilterParameters(page=1, exclude_edition="edB", name="1"))) == \ + assert len(await users_crud.get_users_filtered_page(database_session, + FilterParameters(page=1, exclude_edition="edB", name="1"))) == \ max(count - DB_PAGE_SIZE, 0) - b_users = users_crud.get_users_filtered_page(database_session, - FilterParameters(page=0, exclude_edition="edA", name="1")) + b_users = await users_crud.get_users_filtered_page(database_session, + FilterParameters(page=0, exclude_edition="edA", name="1")) assert len(b_users) == min(count, DB_PAGE_SIZE) for user in b_users: assert "a" not in user.name - assert len(users_crud.get_users_filtered_page(database_session, - FilterParameters(page=1, exclude_edition="edA", name="1"))) == \ + assert len(await users_crud.get_users_filtered_page(database_session, + FilterParameters(page=1, exclude_edition="edA", name="1"))) == \ max(count - DB_PAGE_SIZE, 0) -def test_get_all_users_for_edition_excluded_edition_paginated(database_session: Session): +async def test_get_all_users_for_edition_excluded_edition_paginated(database_session: AsyncSession): edition_a = models.Edition(year=2022, name="edA") edition_b = models.Edition(year=2023, name="edB") database_session.add(edition_a) database_session.add(edition_b) - database_session.commit() + await database_session.commit() correct_users = [] for i in range(round(DB_PAGE_SIZE * 1.5)): @@ -370,43 +380,43 @@ def test_get_all_users_for_edition_excluded_edition_paginated(database_session: user_2 = models.User(name=f"User {i} - b", admin=False) database_session.add(user_1) database_session.add(user_2) - database_session.commit() - database_session.execute(models.user_editions.insert(), [ + await database_session.commit() + await database_session.execute(models.user_editions.insert(), [ {"user_id": user_1.user_id, "edition_id": edition_a.edition_id}, {"user_id": user_2.user_id, "edition_id": edition_b.edition_id}, ]) if i % 2: - database_session.execute(models.user_editions.insert(), [ + await database_session.execute(models.user_editions.insert(), [ {"user_id": user_1.user_id, "edition_id": edition_b.edition_id}, ]) else: correct_users.append(user_1) - database_session.commit() + await database_session.commit() - users = users_crud.get_users_filtered_page(database_session, FilterParameters(page=0, exclude_edition="edB", - edition="edA")) + users = await users_crud.get_users_filtered_page(database_session, FilterParameters(page=0, exclude_edition="edB", + edition="edA")) assert len(users) == len(correct_users) for user in users: assert user in correct_users -def test_edit_admin_status(database_session: Session): +async def test_edit_admin_status(database_session: AsyncSession): """Test changing the admin status of a user""" # Create user user = models.User(name="user1", admin=False) database_session.add(user) - database_session.commit() + await database_session.commit() - users_crud.edit_admin_status(database_session, user.user_id, True) + await users_crud.edit_admin_status(database_session, user.user_id, True) assert user.admin - users_crud.edit_admin_status(database_session, user.user_id, False) + await users_crud.edit_admin_status(database_session, user.user_id, False) assert not user.admin -def test_add_coach(database_session: Session): +async def test_add_coach(database_session: AsyncSession): """Test adding a user as coach""" # Create user @@ -417,15 +427,14 @@ def test_add_coach(database_session: Session): edition = models.Edition(year=1, name="ed1") database_session.add(edition) - database_session.commit() - - users_crud.add_coach(database_session, user.user_id, edition.name) - coach = database_session.query(user_editions).one() + await database_session.commit() + await users_crud.add_coach(database_session, user.user_id, edition.name) + coach = (await database_session.execute(select(user_editions))).one() assert coach.user_id == user.user_id assert coach.edition_id == edition.edition_id -def test_remove_coach(database_session: Session): +async def test_remove_coach(database_session: AsyncSession): """Test removing a user as coach""" # Create user @@ -438,19 +447,19 @@ def test_remove_coach(database_session: Session): edition = models.Edition(year=1, name="ed1") database_session.add(edition) - database_session.commit() + await database_session.commit() # Create coach role - database_session.execute(models.user_editions.insert(), [ + await database_session.execute(models.user_editions.insert(), [ {"user_id": user1.user_id, "edition_id": edition.edition_id}, {"user_id": user2.user_id, "edition_id": edition.edition_id} ]) - users_crud.remove_coach(database_session, user1.user_id, edition.name) - assert len(database_session.query(user_editions).all()) == 1 + await users_crud.remove_coach(database_session, user1.user_id, edition.name) + assert len((await database_session.execute(select(user_editions))).scalars().all()) == 1 -def test_remove_coach_all_editions(database_session: Session): +async def test_remove_coach_all_editions(database_session: AsyncSession): """Test removing a user as coach from all editions""" # Create user @@ -467,21 +476,21 @@ def test_remove_coach_all_editions(database_session: Session): database_session.add(edition2) database_session.add(edition3) - database_session.commit() + await database_session.commit() # Create coach role - database_session.execute(models.user_editions.insert(), [ + await database_session.execute(models.user_editions.insert(), [ {"user_id": user1.user_id, "edition_id": edition1.edition_id}, {"user_id": user1.user_id, "edition_id": edition2.edition_id}, {"user_id": user1.user_id, "edition_id": edition3.edition_id}, {"user_id": user2.user_id, "edition_id": edition2.edition_id}, ]) - users_crud.remove_coach_all_editions(database_session, user1.user_id) - assert len(database_session.query(user_editions).all()) == 1 + await users_crud.remove_coach_all_editions(database_session, user1.user_id) + assert len((await database_session.execute(select(user_editions))).scalars().all()) == 1 -def test_get_all_requests(database_session: Session): +async def test_get_all_requests(database_session: AsyncSession): """Test get request for all userrequests""" # Create user user1 = models.User(name="user1") @@ -495,7 +504,7 @@ def test_get_all_requests(database_session: Session): database_session.add(edition1) database_session.add(edition2) - database_session.commit() + await database_session.commit() # Create request request1 = models.CoachRequest(user_id=user1.user_id, edition_id=edition1.edition_id) @@ -503,9 +512,9 @@ def test_get_all_requests(database_session: Session): database_session.add(request1) database_session.add(request2) - database_session.commit() + await database_session.commit() - requests = users_crud.get_requests(database_session) + requests = await users_crud.get_requests(database_session) assert len(requests) == 2 assert request1 in requests assert request2 in requests @@ -514,7 +523,7 @@ def test_get_all_requests(database_session: Session): assert user2 in users -def test_get_requests_paginated(database_session: Session): +async def test_get_requests_paginated(database_session: AsyncSession): edition = models.Edition(year=2022, name="ed2022") database_session.add(edition) @@ -522,15 +531,15 @@ def test_get_requests_paginated(database_session: Session): user = models.User(name=f"User {i}", admin=False) database_session.add(user) database_session.add(CoachRequest(user=user, edition=edition)) - database_session.commit() + await database_session.commit() - assert len(users_crud.get_requests_page(database_session, 0)) == DB_PAGE_SIZE - assert len(users_crud.get_requests_page(database_session, 1)) == round( + assert len(await users_crud.get_requests_page(database_session, 0)) == DB_PAGE_SIZE + assert len(await users_crud.get_requests_page(database_session, 1)) == round( DB_PAGE_SIZE * 1.5 ) - DB_PAGE_SIZE -def test_get_requests_paginated_filter_user_name(database_session: Session): +async def test_get_requests_paginated_filter_user_name(database_session: AsyncSession): edition = models.Edition(year=2022, name="ed2022") database_session.add(edition) @@ -541,15 +550,15 @@ def test_get_requests_paginated_filter_user_name(database_session: Session): database_session.add(CoachRequest(user=user, edition=edition)) if "1" in str(i): count += 1 - database_session.commit() + await database_session.commit() - assert len(users_crud.get_requests_page(database_session, 0, "1")) == \ + assert len(await users_crud.get_requests_page(database_session, 0, "1")) == \ min(DB_PAGE_SIZE, count) - assert len(users_crud.get_requests_page(database_session, 1, "1")) == \ + assert len(await users_crud.get_requests_page(database_session, 1, "1")) == \ max(count - DB_PAGE_SIZE, 0) -def test_get_all_requests_from_edition(database_session: Session): +async def test_get_all_requests_from_edition(database_session: AsyncSession): """Test get request for all userrequests of a given edition""" # Create user @@ -564,7 +573,7 @@ def test_get_all_requests_from_edition(database_session: Session): database_session.add(edition1) database_session.add(edition2) - database_session.commit() + await database_session.commit() # Create request request1 = models.CoachRequest(user_id=user1.user_id, edition_id=edition1.edition_id) @@ -572,18 +581,18 @@ def test_get_all_requests_from_edition(database_session: Session): database_session.add(request1) database_session.add(request2) - database_session.commit() + await database_session.commit() - requests = users_crud.get_requests_for_edition(database_session, edition1.name) + requests = await users_crud.get_requests_for_edition(database_session, edition1.name) assert len(requests) == 1 assert requests[0].user == user1 - requests = users_crud.get_requests_for_edition(database_session, edition2.name) + requests = await users_crud.get_requests_for_edition(database_session, edition2.name) assert len(requests) == 1 assert requests[0].user == user2 -def test_get_requests_for_edition_paginated(database_session: Session): +async def test_get_requests_for_edition_paginated(database_session: AsyncSession): edition = models.Edition(year=2022, name="ed2022") database_session.add(edition) @@ -591,15 +600,15 @@ def test_get_requests_for_edition_paginated(database_session: Session): user = models.User(name=f"User {i}", admin=False) database_session.add(user) database_session.add(CoachRequest(user=user, edition=edition)) - database_session.commit() + await database_session.commit() - assert len(users_crud.get_requests_for_edition_page(database_session, edition.name, 0)) == DB_PAGE_SIZE - assert len(users_crud.get_requests_for_edition_page(database_session, edition.name, 1)) == round( + assert len(await users_crud.get_requests_for_edition_page(database_session, edition.name, 0)) == DB_PAGE_SIZE + assert len(await users_crud.get_requests_for_edition_page(database_session, edition.name, 1)) == round( DB_PAGE_SIZE * 1.5 ) - DB_PAGE_SIZE -def test_get_requests_for_edition_paginated_filter_user_name(database_session: Session): +async def test_get_requests_for_edition_paginated_filter_user_name(database_session: AsyncSession): edition = models.Edition(year=2022, name="ed2022") database_session.add(edition) @@ -610,15 +619,15 @@ def test_get_requests_for_edition_paginated_filter_user_name(database_session: S database_session.add(CoachRequest(user=user, edition=edition)) if "1" in str(i): count += 1 - database_session.commit() + await database_session.commit() - assert len(users_crud.get_requests_for_edition_page(database_session, edition.name, 0, "1")) == \ + assert len(await users_crud.get_requests_for_edition_page(database_session, edition.name, 0, "1")) == \ min(DB_PAGE_SIZE, count) - assert len(users_crud.get_requests_for_edition_page(database_session, edition.name, 1, "1")) == \ + assert len(await users_crud.get_requests_for_edition_page(database_session, edition.name, 1, "1")) == \ max(count - DB_PAGE_SIZE, 0) -def test_accept_request(database_session: Session): +async def test_accept_request(database_session: AsyncSession): """Test accepting a coach request""" # Create user @@ -629,23 +638,23 @@ def test_accept_request(database_session: Session): edition1 = models.Edition(year=1, name="ed1") database_session.add(edition1) - database_session.commit() + await database_session.commit() # Create request request1 = models.CoachRequest(user_id=user1.user_id, edition_id=edition1.edition_id) database_session.add(request1) - database_session.commit() + await database_session.commit() - users_crud.accept_request(database_session, request1.request_id) + await users_crud.accept_request(database_session, request1.request_id) - requests = database_session.query(CoachRequest).all() + requests = (await database_session.execute(select(CoachRequest))).scalars().all() assert len(requests) == 0 assert user1.editions[0].edition_id == edition1.edition_id -def test_reject_request_new_user(database_session: Session): +async def test_reject_request_new_user(database_session: AsyncSession): """Test rejecting a coach request""" # Create user @@ -655,50 +664,53 @@ def test_reject_request_new_user(database_session: Session): # Create edition edition1 = models.Edition(year=1, name="ed2022") database_session.add(edition1) - database_session.commit() + await database_session.commit() # Create request request1 = models.CoachRequest(user_id=user1.user_id, edition_id=edition1.edition_id) database_session.add(request1) - database_session.commit() + await database_session.commit() - users_crud.reject_request(database_session, request1.request_id) + await users_crud.reject_request(database_session, request1.request_id) - requests = database_session.query(CoachRequest).all() + requests = (await database_session.execute(select(CoachRequest))).scalars().all() assert len(requests) == 0 -def test_remove_request_if_exists_exists(database_session: Session): +async def test_remove_request_if_exists_exists(database_session: AsyncSession): """Test deleting a request when it exists""" user = models.User(name="user1") database_session.add(user) edition = models.Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() request = models.CoachRequest(user_id=user.user_id, edition_id=edition.edition_id) database_session.add(request) - database_session.commit() + await database_session.commit() - assert database_session.query(CoachRequest).count() == 1 + count = (await database_session.execute(select(func.count()).select_from(select(CoachRequest).subquery()))).scalar_one() + assert count == 1 # Remove the request - users_crud.remove_request_if_exists(database_session, user.user_id, edition.name) + await users_crud.remove_request_if_exists(database_session, user.user_id, edition.name) - assert database_session.query(CoachRequest).count() == 0 + count = ( + await database_session.execute(select(func.count()).select_from(select(CoachRequest).subquery()))).scalar_one() + assert count == 0 -def test_remove_request_if_not_exists(database_session: Session): +async def test_remove_request_if_not_exists(database_session: AsyncSession): """Test deleting a request when it doesn't exist""" user = models.User(name="user1") database_session.add(user) edition = models.Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() # Remove the request # If the test succeeds then it means no error was raised, even though the request # doesn't exist - users_crud.remove_request_if_exists(database_session, user.user_id, edition.name) + await users_crud.remove_request_if_exists(database_session, user.user_id, edition.name) diff --git a/backend/tests/test_database/test_models.py b/backend/tests/test_database/test_models.py index 662217c4e..09347556f 100644 --- a/backend/tests/test_database/test_models.py +++ b/backend/tests/test_database/test_models.py @@ -1,52 +1,58 @@ -from sqlalchemy.orm import Session +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from src.database import models -def test_user_coach_request(database_session: Session): +async def test_user_coach_request(database_session: AsyncSession): """Test sending a coach request""" edition = models.Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() # Passing as user_id user = models.User(name="name") database_session.add(user) - database_session.commit() + await database_session.commit() req = models.CoachRequest(user_id=user.user_id, edition=edition) database_session.add(req) - database_session.commit() + await database_session.commit() assert req.user == user # Check if passing as user instead of user_id works user = models.User(name="name") database_session.add(user) - database_session.commit() + await database_session.commit() req = models.CoachRequest(user=user, edition=edition) database_session.add(req) - database_session.commit() + await database_session.commit() assert req.user_id == user.user_id -def test_project_partners(database_session: Session): +async def test_project_partners(database_session: AsyncSession): """Test adding a partner to a project""" project = models.Project(name="project") database_session.add(project) - database_session.commit() + await database_session.commit() partner = models.Partner(name="partner") - database_session.add(project) - database_session.commit() + database_session.add(partner) + await database_session.commit() + + # query the partner and the project to create the association tables + (await database_session.execute(select(models.Partner).where(models.Partner.partner_id == partner.partner_id))).unique().scalars().one() + (await database_session.execute( + select(models.Project).where(models.Project.project_id == project.project_id))).unique().scalars().one() assert len(partner.projects) == 0 assert len(project.partners) == 0 partner.projects.append(project) - database_session.commit() + await database_session.commit() # Verify that appending to the list updates the association table # in both directions diff --git a/backend/tests/test_fill_database.py b/backend/tests/test_fill_database.py index d68444d8e..0f23f36af 100644 --- a/backend/tests/test_fill_database.py +++ b/backend/tests/test_fill_database.py @@ -1,6 +1,7 @@ -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from tests.fill_database import fill_database -def test_fill_database(database_session: Session): + +async def test_fill_database(database_session: AsyncSession): """Test that fill_database don't give an error""" - fill_database(database_session) + await fill_database(database_session) diff --git a/backend/tests/test_logic/test_oauth/__init__.py b/backend/tests/test_logic/test_oauth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/tests/test_logic/test_oauth/test_github.py b/backend/tests/test_logic/test_oauth/test_github.py new file mode 100644 index 000000000..1188143b9 --- /dev/null +++ b/backend/tests/test_logic/test_oauth/test_github.py @@ -0,0 +1,232 @@ +from unittest.mock import AsyncMock + +import sqlalchemy.exc +from pydantic import ValidationError +import pytest +from sqlalchemy.ext.asyncio import AsyncSession + +from src.app.exceptions.register import InvalidGitHubCode +from src.app.logic.oauth import github as logic +from src.app.logic.oauth.github import get_github_profile, get_github_id, get_user_by_github_code +from src.database.models import User, AuthGitHub + + +async def test_get_github_access_token_valid(): + """Test getting the access token when the response succeeds""" + http_session = AsyncMock() + response = AsyncMock() + http_session.post.return_value = response + response.json.return_value = {"access_token": "at", "scope": "read:user,user:email"} + + # If this doesn't throw an error then everything is okay + await logic.get_github_access_token(http_session, "code") + + +async def test_get_github_access_token_error(): + """Test getting the access token when the response fails""" + http_session = AsyncMock() + response = AsyncMock() + http_session.post.return_value = response + response.json.return_value = {"error": "exists", "error_description": "description"} + + with pytest.raises(InvalidGitHubCode): + await logic.get_github_access_token(http_session, "code") + + +async def test_get_github_access_token_missing_scopes(): + """Test getting the access token when the user removes some required scopes""" + http_session = AsyncMock() + response = AsyncMock() + http_session.post.return_value = response + response.json.return_value = {"access_token": "at", "scope": "read:user"} + + with pytest.raises(ValidationError): + await logic.get_github_access_token(http_session, "code") + + +async def test_get_github_profile_public_email(): + """Test getting the user's GitHub profile when the email is public""" + http_session = AsyncMock() + first_response = AsyncMock() + + first_response.status = 200 + first_response.json.return_value = { + "name": "My Name", + "email": "my@email.address", + "id": 48 + } + + http_session.get.return_value = first_response + + profile = await get_github_profile(http_session, "token") + + assert profile.name == "My Name" + assert profile.email == "my@email.address" + + # Verify that the second request was NOT sent: + # mock only awaited once + http_session.get.assert_awaited_once() + + +async def test_get_github_profile_no_name_uses_login(): + """Test getting the user's GitHub profile when the name is None""" + http_session = AsyncMock() + response = AsyncMock() + + response.status = 200 + response.json.return_value = { + "name": None, + "email": "my@email.address", + "login": "login", + "id": 48 + } + + http_session.get.return_value = response + + profile = await get_github_profile(http_session, "token") + + assert profile.name == "login" + + +async def test_get_github_profile_private_email(): + """Test getting a user's GitHub profile when their email address is private""" + http_session = AsyncMock() + first_response = AsyncMock() + + first_response.status = 200 + first_response.json.return_value = { + "name": "My Name", + "email": None, + "id": 48 + } + + second_response = AsyncMock() + second_response.status = 200 + second_response.json.return_value = [ + { + "primary": False, "email": "secondary@email.com" + }, + { + "primary": True, "email": "primary@email.com" + } + ] + + http_session.get.side_effect = [first_response, second_response] + + profile = await get_github_profile(http_session, "token") + + # Primary email was used + assert profile.email == "primary@email.com" + assert http_session.get.await_count == 2 + + +async def test_get_github_profile_no_primary_email(): + """Test getting a user's GitHub profile when they have no primary email set + Not sure if this is possible but better safe than sorry + """ + http_session = AsyncMock() + first_response = AsyncMock() + + first_response.status = 200 + first_response.json.return_value = { + "name": "My Name", + "email": None, + "id": 48 + } + + second_response = AsyncMock() + second_response.status = 200 + second_response.json.return_value = [ + { + "primary": False, "email": "secondary@email.com" + }, + { + "primary": False, "email": "primary@email.com" + } + ] + + http_session.get.side_effect = [first_response, second_response] + + profile = await get_github_profile(http_session, "token") + + # No primary email, this should now default to the first entry in the list + assert profile.email == "secondary@email.com" + assert http_session.get.await_count == 2 + + +async def test_get_github_id(): + """Test getting a user's GitHub user id""" + http_session = AsyncMock() + response = AsyncMock() + + response.status = 200 + response.json.return_value = { + "id": 1 + } + + http_session.get.return_value = response + + user_id = await get_github_id(http_session, "token") + assert user_id == 1 + + +async def test_get_user_by_github_code_exists(database_session: AsyncSession): + """Test getting a user by their GitHub code""" + user = User(name="name", admin=True) + database_session.add(user) + gh_auth = AuthGitHub(access_token="token", email="email", github_user_id=1, user=user) + database_session.add(gh_auth) + await database_session.commit() + + http_session = AsyncMock() + + # Request that gets an access token + first_response = AsyncMock() + first_response.status = 200 + first_response.json.return_value = { + "access_token": "token", + "scope": "read:user,user:email" + } + + # Request that gets the user's id + second_response = AsyncMock() + second_response.status = 200 + second_response.json.return_value = { + "name": "name", + "email": "email", + "id": 1 + } + + http_session.post.return_value = first_response + http_session.get.return_value = second_response + + found_user = await get_user_by_github_code(http_session, database_session, "some code") + assert found_user.user_id == user.user_id + + +async def test_get_user_by_github_code_doesnt_exist(database_session: AsyncSession): + """Test getting a user by their GitHub code when we don't know the user""" + http_session = AsyncMock() + + # Request that gets an access token + first_response = AsyncMock() + first_response.status = 200 + first_response.json.return_value = { + "access_token": "token", + "scope": "read:user,user:email" + } + + # Request that gets the user's id + second_response = AsyncMock() + second_response.status = 200 + second_response.json.return_value = { + "name": "name", + "email": "email", + "id": 1 + } + + http_session.post.return_value = first_response + http_session.get.return_value = second_response + + with pytest.raises(sqlalchemy.exc.NoResultFound): + await get_user_by_github_code(http_session, database_session, "some code") diff --git a/backend/tests/test_logic/test_register.py b/backend/tests/test_logic/test_register.py index 02dea4cd5..f2817d947 100644 --- a/backend/tests/test_logic/test_register.py +++ b/backend/tests/test_logic/test_register.py @@ -1,37 +1,45 @@ -import pytest -from sqlalchemy.orm import Session -from sqlalchemy.exc import NoResultFound +import uuid +from unittest.mock import patch -from src.app.schemas.register import NewUser -from src.database.models import AuthEmail, CoachRequest, User, Edition, InviteLink +import pytest +from pydantic import ValidationError +from sqlalchemy.exc import NoResultFound, SQLAlchemyError +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession -from src.app.logic.register import create_request +from src.app.exceptions.crud import DuplicateInsertException from src.app.exceptions.register import FailedToAddNewUserException +from src.app.logic.register import create_request_email, create_request_github +from src.app.schemas.oauth.github import GitHubProfile +from src.app.schemas.register import EmailRegister +from src.database.models import AuthEmail, CoachRequest, User, Edition, InviteLink -def test_create_request(database_session: Session): +async def test_create_request_email(database_session: AsyncSession): """Tests if a normal request can be created""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) invite_link: InviteLink = InviteLink( edition=edition, target_email="jw@gmail.com") - database_session.commit() - new_user = NewUser(name="jos", email="email@email.com", - pw="wachtwoord", uuid=invite_link.uuid) - create_request(database_session, new_user, edition) + database_session.add(invite_link) + await database_session.commit() + new_user = EmailRegister(name="jos", email="email@email.com", + pw="wachtwoord", uuid=invite_link.uuid) + await create_request_email(database_session, new_user, edition) - users = database_session.query(User).where(User.name == "jos").all() + users = (await database_session.execute(select(User).where(User.name == "jos"))).unique().scalars().all() assert len(users) == 1 - coach_requests = database_session.query( - CoachRequest).where(CoachRequest.user == users[0]).all() - auth_email = database_session.query(AuthEmail).where( - AuthEmail.user == users[0]).all() + coach_requests = (await database_session.execute(select( + CoachRequest).where(CoachRequest.user == users[0]))).unique().scalars().all() + auth_email = (await database_session.execute(select(AuthEmail).where( + AuthEmail.user == users[0]))).scalars().all() assert len(coach_requests) == 1 assert auth_email[0].pw_hash != new_user.pw assert len(auth_email) == 1 -def test_duplicate_user(database_session: Session): +@pytest.mark.skip(reason="The async database rolls back both, even with nested query") +async def test_duplicate_user(database_session: AsyncSession): """Tests if there is a duplicate, it's not created in the database""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) @@ -39,61 +47,148 @@ def test_duplicate_user(database_session: Session): edition=edition, target_email="jw@gmail.com") invite_link2: InviteLink = InviteLink( edition=edition, target_email="jw@gmail.com") - database_session.commit() - nu1 = NewUser(name="user1", email="email@email.com", - pw="wachtwoord1", uuid=invite_link1.uuid) - nu2 = NewUser(name="user2", email="email@email.com", - pw="wachtwoord2", uuid=invite_link2.uuid) + database_session.add(invite_link1) + database_session.add(invite_link2) + await database_session.commit() + nu1 = EmailRegister(name="user1", email="email@email.com", + pw="wachtwoord1", uuid=invite_link1.uuid) + nu2 = EmailRegister(name="user2", email="email@email.com", + pw="wachtwoord2", uuid=invite_link2.uuid) # These two have to be nested transactions because they share the same database_session, # and otherwise the second one rolls the first one back # Making them nested transactions creates a savepoint so only that part is rolled back - with database_session.begin_nested(): - create_request(database_session, nu1, edition) + async with database_session.begin_nested(): + await create_request_email(database_session, nu1, edition) - with pytest.raises(FailedToAddNewUserException), database_session.begin_nested(): - create_request(database_session, nu2, edition) + async with database_session.begin_nested(): + with pytest.raises(DuplicateInsertException): + await create_request_email(database_session, nu2, edition) # Verify that second user wasn't added # the first addition was successful, the second wasn't - users = database_session.query(User).all() + users = (await database_session.execute(select(User))).unique().scalars().all() assert len(users) == 1 assert users[0].name == nu1.name - emails = database_session.query(AuthEmail).all() + emails = (await database_session.execute(select(AuthEmail))).scalars().all() assert len(emails) == 1 assert emails[0].user == users[0] - requests = database_session.query(CoachRequest).all() + requests = (await database_session.execute(select(CoachRequest))).unique().scalars().all() assert len(requests) == 1 assert requests[0].user == users[0] # Verify that the link wasn't removed - links = database_session.query(InviteLink).all() + links = (await database_session.execute(select(InviteLink))).scalars().all() assert len(links) == 1 -def test_use_same_uuid_multiple_times(database_session: Session): +async def test_email_exception_doesnt_add(database_session: AsyncSession): + """Test that if another exception is raised, we handle it correctly""" + edition = Edition(year=2022, name="ed2022") + database_session.add(edition) + invite_link: InviteLink = InviteLink( + edition=edition, target_email="jw@gmail.com") + database_session.add(invite_link) + await database_session.commit() + nu = EmailRegister(name="user", email="email@email.com", + pw="wachtwoord", uuid=invite_link.uuid) + + with patch("src.app.logic.register.create_auth_email", side_effect=SQLAlchemyError("mocked")): + with pytest.raises(FailedToAddNewUserException): + await create_request_email(database_session, nu, edition) + + requests = (await database_session.execute(select(CoachRequest))).all() + assert len(requests) == 0 + + +async def test_use_same_uuid_multiple_times(database_session: AsyncSession): """Tests that you can't use the same UUID multiple times""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) invite_link: InviteLink = InviteLink( edition=edition, target_email="jw@gmail.com") - database_session.commit() - new_user1 = NewUser(name="jos", email="email@email.com", - pw="wachtwoord", uuid=invite_link.uuid) - create_request(database_session, new_user1, edition) + database_session.add(invite_link) + await database_session.commit() + new_user1 = EmailRegister(name="jos", email="email@email.com", + pw="wachtwoord", uuid=invite_link.uuid) + await create_request_email(database_session, new_user1, edition) with pytest.raises(NoResultFound): - new_user2 = NewUser(name="jos", email="email2@email.com", - pw="wachtwoord", uuid=invite_link.uuid) - create_request(database_session, new_user2, edition) + new_user2 = EmailRegister(name="jos", email="email2@email.com", + pw="wachtwoord", uuid=invite_link.uuid) + await create_request_email(database_session, new_user2, edition) + + +async def test_not_a_correct_email(database_session: AsyncSession): + """Tests when the email is not a correct email address, it gets the right error""" + edition = Edition(year=2022, name="ed2022") + database_session.add(edition) + await database_session.commit() + with pytest.raises(ValidationError): + new_user = EmailRegister(name="jos", email="email", pw="wachtwoord", uuid=uuid.uuid4()) + await create_request_email(database_session, new_user, edition) + + +async def test_create_request_github(database_session: AsyncSession): + """Test creating a new request using GitHub OAuth""" + edition = Edition(year=2022, name="ed2022") + database_session.add(edition) + invite = InviteLink(edition=edition, target_email="a@b.c") + database_session.add(invite) + await database_session.commit() + + profile = GitHubProfile(access_token="", email="email@addre.ss", id=1, name="Name") + await create_request_github(database_session, profile, invite.uuid, edition) + + users = (await database_session.execute(select(User).where(User.name == "Name"))).unique().scalars().all() + + assert len(users) == 1 + assert users[0].github_auth is not None + assert users[0].github_auth.github_user_id == 1 + + +async def test_create_request_github_duplicate(database_session: AsyncSession): + """Test creating a request with an already-existing GitHub user""" + edition = Edition(year=2022, name="ed2022") + database_session.add(edition) + invite = InviteLink(edition=edition, target_email="a@b.c") + database_session.add(invite) + await database_session.commit() + profile = GitHubProfile(access_token="", email="email@addre.ss", id=1, name="Name") + await create_request_github(database_session, profile, invite.uuid, edition) -def test_not_a_correct_email(database_session: Session): - """Tests when the email is not a correct email adress, it's get the right error""" + users = (await database_session.execute(select(User).where(User.name == "Name"))).unique().scalars().all() + + assert len(users) == 1 + + invite = InviteLink(edition=edition, target_email="a@b.c") + database_session.add(invite) + await database_session.commit() + + # Change everything but re-use the same GitHub userid + profile.access_token = "another access token" + profile.name = "another name" + profile.email = "another@email.address" + + with pytest.raises(DuplicateInsertException): + await create_request_github(database_session, profile, invite.uuid, edition) + + +async def test_github_exception_doesnt_add(database_session: AsyncSession): + """Test that if another exception is raised, we handle it correctly""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() - with pytest.raises(ValueError): - new_user = NewUser(name="jos", email="email", pw="wachtwoord") - create_request(database_session, new_user, edition) + invite = InviteLink(edition=edition, target_email="a@b.c") + database_session.add(invite) + await database_session.commit() + + profile = GitHubProfile(access_token="", email="email@addre.ss", id=1, name="Name") + + with patch("src.app.logic.register.create_auth_github", side_effect=SQLAlchemyError("mocked")): + with pytest.raises(FailedToAddNewUserException): + await create_request_github(database_session, profile, invite.uuid, edition) + + requests = (await database_session.execute(select(CoachRequest))).scalars().all() + assert len(requests) == 0 diff --git a/backend/tests/test_routers/test_editions/test_editions/test_editions.py b/backend/tests/test_routers/test_editions/test_editions/test_editions.py index 9c3a2856c..a23dd5624 100644 --- a/backend/tests/test_routers/test_editions/test_editions/test_editions.py +++ b/backend/tests/test_routers/test_editions/test_editions/test_editions.py @@ -1,4 +1,5 @@ -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status from settings import DB_PAGE_SIZE @@ -6,7 +7,7 @@ from tests.utils.authorization import AuthClient -def test_get_editions(database_session: Session, auth_client: AuthClient): +async def test_get_editions(database_session: AsyncSession, auth_client: AuthClient): """Perform tests on getting editions Args: @@ -15,12 +16,14 @@ def test_get_editions(database_session: Session, auth_client: AuthClient): """ edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() - auth_client.coach(edition) + await auth_client.coach(edition) # Make the get request - response = auth_client.get("/editions/") + async with auth_client: + response = await auth_client.get("/editions/", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK response = response.json() @@ -29,35 +32,37 @@ def test_get_editions(database_session: Session, auth_client: AuthClient): assert response["editions"][0]["name"] == "ed2022" -def test_get_editions_paginated(database_session: Session, auth_client: AuthClient): +async def test_get_editions_paginated(database_session: AsyncSession, auth_client: AuthClient): """Perform tests on getting paginated editions""" for i in range(round(DB_PAGE_SIZE * 1.5)): database_session.add(Edition(name=f"Project {i}", year=i)) - database_session.commit() + await database_session.commit() - auth_client.admin() + await auth_client.admin() - response = auth_client.get("/editions?page=0") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['editions']) == DB_PAGE_SIZE - response = auth_client.get("/editions?page=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['editions']) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + async with auth_client: + response = await auth_client.get("/editions?page=0", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['editions']) == DB_PAGE_SIZE + response = await auth_client.get("/editions?page=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['editions']) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE -def test_get_edition_by_name_admin(database_session: Session, auth_client: AuthClient): +async def test_get_edition_by_name_admin(database_session: AsyncSession, auth_client: AuthClient): """Test getting an edition as an admin""" - auth_client.admin() + await auth_client.admin() edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() - response = auth_client.get(f"/editions/{edition.name}") + async with auth_client: + response = await auth_client.get(f"/editions/{edition.name}") assert response.status_code == status.HTTP_200_OK -def test_get_edition_by_name_coach(database_session: Session, auth_client: AuthClient): +async def test_get_edition_by_name_coach(database_session: AsyncSession, auth_client: AuthClient): """Perform tests on getting editions by ids Args: @@ -66,28 +71,31 @@ def test_get_edition_by_name_coach(database_session: Session, auth_client: AuthC """ edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() - auth_client.coach(edition) + await auth_client.coach(edition) # Make the get request - response = auth_client.get(f"/editions/{edition.name}") - assert response.status_code == status.HTTP_200_OK - assert response.json()["year"] == 2022 - assert response.json()["editionId"] == edition.edition_id - assert response.json()["name"] == edition.name + async with auth_client: + response = await auth_client.get(f"/editions/{edition.name}") + assert response.status_code == status.HTTP_200_OK + assert response.json()["year"] == 2022 + assert response.json()["editionId"] == edition.edition_id + assert response.json()["name"] == edition.name -def test_get_edition_by_name_unauthorized(database_session: Session, auth_client: AuthClient): +async def test_get_edition_by_name_unauthorized(database_session: AsyncSession, auth_client: AuthClient): """Test getting an edition without access token""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() - assert auth_client.get("/editions/ed2022").status_code == status.HTTP_401_UNAUTHORIZED + async with auth_client: + response = await auth_client.get("/editions/ed2022") + assert response.status_code == status.HTTP_401_UNAUTHORIZED -def test_get_edition_by_name_not_coach(database_session: Session, auth_client: AuthClient): +async def test_get_edition_by_name_not_coach(database_session: AsyncSession, auth_client: AuthClient): """Test getting an edition without being a coach in it""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) @@ -95,125 +103,139 @@ def test_get_edition_by_name_not_coach(database_session: Session, auth_client: A coach_edition = Edition(year=2021, name="ed2021") database_session.add(coach_edition) - database_session.commit() + await database_session.commit() # Sign in as a coach in a different edition - auth_client.coach(coach_edition) + await auth_client.coach(coach_edition) - assert auth_client.get(f"/editions/{edition.name}").status_code == status.HTTP_403_FORBIDDEN + async with auth_client: + response = await auth_client.get(f"/editions/{edition.name}") + assert response.status_code == status.HTTP_403_FORBIDDEN -def test_create_edition_admin(database_session: Session, auth_client: AuthClient): +async def test_create_edition_admin(database_session: AsyncSession, auth_client: AuthClient): """Test creating an edition as an admin""" - auth_client.admin() - - # Verify that editions doesn't exist yet - assert auth_client.get("/editions/ed2022/").status_code == status.HTTP_404_NOT_FOUND - - # Make the post request - response = auth_client.post("/editions/", json={"year": 2022, "name": "ed2022"}) - assert response.status_code == status.HTTP_201_CREATED - assert auth_client.get("/editions/").json()["editions"][0]["year"] == 2022 - assert auth_client.get("/editions/").json()["editions"][0]["editionId"] == 1 - assert auth_client.get("/editions/").json()["editions"][0]["name"] == "ed2022" - assert auth_client.get("/editions/ed2022/").status_code == status.HTTP_200_OK - - -def test_create_edition_unauthorized(database_session: Session, auth_client: AuthClient): + await auth_client.admin() + + async with auth_client: + # Verify that editions doesn't exist yet + response = await auth_client.get("/editions/ed2022") + assert response.status_code == status.HTTP_404_NOT_FOUND + # Make the post request + response = await auth_client.post("/editions", json={"year": 2022, "name": "ed2022"}) + assert response.status_code == status.HTTP_201_CREATED + response = await auth_client.get("/editions/", follow_redirects=True) + assert response.json()["editions"][0]["year"] == 2022 + assert response.json()["editions"][0]["editionId"] == 1 + assert response.json()["editions"][0]["name"] == "ed2022" + response = await auth_client.get("/editions/ed2022") + assert response.status_code == status.HTTP_200_OK + + +async def test_create_edition_unauthorized(database_session: AsyncSession, auth_client: AuthClient): """Test creating an edition without any credentials""" - assert auth_client.post("/editions/", json={"year": 2022, "name": "ed2022"}).status_code == status.HTTP_401_UNAUTHORIZED + async with auth_client: + response = await auth_client.post("/editions", json={"year": 2022, "name": "ed2022"}) + assert response.status_code == status.HTTP_401_UNAUTHORIZED -def test_create_edition_coach(database_session: Session, auth_client: AuthClient): +async def test_create_edition_coach(database_session: AsyncSession, auth_client: AuthClient): """Test creating an edition as a coach""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() - - auth_client.coach(edition) + await database_session.commit() - assert auth_client.post("/editions/", json={"year": 2022, "name": "ed2022"}).status_code == status.HTTP_403_FORBIDDEN + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.post("/editions/", json={"year": 2022, "name": "ed2022"}, follow_redirects=True) + assert response.status_code == status.HTTP_403_FORBIDDEN -def test_create_edition_existing_year(database_session: Session, auth_client: AuthClient): +async def test_create_edition_existing_year(database_session: AsyncSession, auth_client: AuthClient): """Test that creating an edition for a year that already exists throws an error""" - auth_client.admin() - - response = auth_client.post("/editions/", json={"year": 2022, "name": "ed2022"}) - assert response.status_code == status.HTTP_201_CREATED + await auth_client.admin() - # Try to make an edition in the same year - response = auth_client.post("/editions/", json={"year": 2022, "name": "ed2022"}) - assert response.status_code == status.HTTP_409_CONFLICT + async with auth_client: + response = await auth_client.post("/editions", json={"year": 2022, "name": "ed2022"}) + assert response.status_code == status.HTTP_201_CREATED + # Try to make an edition in the same year + response = await auth_client.post("/editions", json={"year": 2022, "name": "ed2022"}) + assert response.status_code == status.HTTP_409_CONFLICT -def test_create_edition_malformed(database_session: Session, auth_client: AuthClient): - auth_client.admin() +async def test_create_edition_malformed(database_session: AsyncSession, auth_client: AuthClient): + await auth_client.admin() - response = auth_client.post("/editions/", json={"year": 2023, "name": "Life is fun"}) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + async with auth_client: + response = await auth_client.post("/editions", json={"year": 2023, "name": "Life is fun"}) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY -def test_delete_edition_admin(database_session: Session, auth_client: AuthClient): +async def test_delete_edition_admin(database_session: AsyncSession, auth_client: AuthClient): """Perform tests on deleting editions Args: database_session (Session): a connection with the database auth_client (AuthClient): a client used to do rest calls """ - auth_client.admin() + await auth_client.admin() edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() - # Make the delete request - response = auth_client.delete(f"/editions/{edition.name}") - assert response.status_code == status.HTTP_204_NO_CONTENT + async with auth_client: + # Make the delete request + response = await auth_client.delete(f"/editions/{edition.name}") + assert response.status_code == status.HTTP_204_NO_CONTENT -def test_delete_edition_unauthorized(database_session: Session, auth_client: AuthClient): +async def test_delete_edition_unauthorized(database_session: AsyncSession, auth_client: AuthClient): """Test deleting an edition without any credentials""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() - # Make the delete request - assert auth_client.delete(f"/editions/{edition.name}").status_code == status.HTTP_401_UNAUTHORIZED + async with auth_client: + # Make the delete request + assert (await auth_client.delete(f"/editions/{edition.name}")).status_code == status.HTTP_401_UNAUTHORIZED -def test_delete_edition_coach(database_session: Session, auth_client: AuthClient): +async def test_delete_edition_coach(database_session: AsyncSession, auth_client: AuthClient): """Test deleting an edition as a coach""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() - auth_client.coach(edition) + await auth_client.coach(edition) - # Make the delete request - assert auth_client.delete(f"/editions/{edition.name}").status_code == status.HTTP_403_FORBIDDEN + async with auth_client: + # Make the delete request + assert (await auth_client.delete(f"/editions/{edition.name}")).status_code == status.HTTP_403_FORBIDDEN -def test_delete_edition_non_existing(database_session: Session, auth_client: AuthClient): +async def test_delete_edition_non_existing(database_session: AsyncSession, auth_client: AuthClient): """Delete an edition that doesn't exist""" - auth_client.admin() + await auth_client.admin() - response = auth_client.delete("/edition/doesnotexist") - assert response.status_code == status.HTTP_404_NOT_FOUND + async with auth_client: + response = await auth_client.delete("/edition/doesnotexist") + assert response.status_code == status.HTTP_404_NOT_FOUND -def test_get_editions_limited_permission(database_session: Session, auth_client: AuthClient): +async def test_get_editions_limited_permission(database_session: AsyncSession, auth_client: AuthClient): """A coach should only see the editions they are drafted for""" edition = Edition(year=2022, name="ed2022") edition2 = Edition(year=2023, name="ed2023") database_session.add(edition) database_session.add(edition2) - database_session.commit() + await database_session.commit() - auth_client.coach(edition) + await auth_client.coach(edition) - # Make the get request - response = auth_client.get("/editions/") + async with auth_client: + # Make the get request + response = await auth_client.get("/editions/", follow_redirects=True) assert response.status_code == status.HTTP_200_OK response = response.json() @@ -223,16 +245,32 @@ def test_get_editions_limited_permission(database_session: Session, auth_client: assert len(response["editions"]) == 1 -def test_get_edition_by_name_coach_not_assigned(database_session: Session, auth_client: AuthClient): +async def test_get_edition_by_name_coach_not_assigned(database_session: AsyncSession, auth_client: AuthClient): """A coach not assigned to the edition should not be able to see it""" edition = Edition(year=2022, name="ed2022") edition2 = Edition(year=2023, name="ed2023") database_session.add(edition) database_session.add(edition2) - database_session.commit() + await database_session.commit() - auth_client.coach(edition) + await auth_client.coach(edition) - # Make the get request - response = auth_client.get(f"/editions/{edition2.name}") + async with auth_client: + # Make the get request + response = await auth_client.get(f"/editions/{edition2.name}") assert response.status_code == status.HTTP_403_FORBIDDEN + + +async def test_patch_edition(database_session: AsyncSession, auth_client: AuthClient): + """Test changing the status of an edition""" + edition = Edition(year=2022, name="ed2022") + database_session.add(edition) + await database_session.commit() + await auth_client.admin() + + async with auth_client: + response = await auth_client.patch(f"/editions/{edition.name}", json={"readonly": True}) + assert response.status_code == status.HTTP_204_NO_CONTENT + + response = await auth_client.get(f"/editions/{edition.name}") + assert response.json()["readonly"] diff --git a/backend/tests/test_routers/test_editions/test_invites/test_invites.py b/backend/tests/test_routers/test_editions/test_invites/test_invites.py index 64f6958b3..ffca102bc 100644 --- a/backend/tests/test_routers/test_editions/test_invites/test_invites.py +++ b/backend/tests/test_routers/test_editions/test_invites/test_invites.py @@ -1,7 +1,7 @@ from json import dumps from uuid import UUID -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status from settings import DB_PAGE_SIZE @@ -9,30 +9,31 @@ from tests.utils.authorization import AuthClient -def test_get_empty_invites(database_session: Session, auth_client: AuthClient): +async def test_get_empty_invites(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting invites when db is empty""" - auth_client.admin() + await auth_client.admin() database_session.add(Edition(year=2022, name="ed2022")) - database_session.commit() + await database_session.commit() - response = auth_client.get("/editions/ed2022/invites") + async with auth_client: + response = await auth_client.get("/editions/ed2022/invites", follow_redirects=True) assert response.status_code == status.HTTP_200_OK assert response.json() == {"inviteLinks": []} -def test_get_invites(database_session: Session, auth_client: AuthClient): +async def test_get_invites(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting invites when db is not empty""" - auth_client.admin() + await auth_client.admin() edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() database_session.add(InviteLink(target_email="test@ema.il", edition=edition)) - database_session.commit() + await database_session.commit() - response = auth_client.get("/editions/ed2022/invites") + async with auth_client: + response = await auth_client.get("/editions/ed2022/invites", follow_redirects=True) - print(response.json()) assert response.status_code == status.HTTP_200_OK json = response.json() assert len(json["inviteLinks"]) == 1 @@ -41,148 +42,156 @@ def test_get_invites(database_session: Session, auth_client: AuthClient): assert link["email"] == "test@ema.il" -def test_get_invites_paginated(database_session: Session, auth_client: AuthClient): +async def test_get_invites_paginated(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting paginated invites when db is not empty""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) for i in range(round(DB_PAGE_SIZE * 1.5)): database_session.add(InviteLink(target_email=f"{i}@example.com", edition=edition)) - database_session.commit() + await database_session.commit() - auth_client.admin() - - response = auth_client.get("/editions/ed2022/invites?page=0") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['inviteLinks']) == DB_PAGE_SIZE - response = auth_client.get("/editions/ed2022/invites?page=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['inviteLinks']) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + await auth_client.admin() + async with auth_client: + response = await auth_client.get("/editions/ed2022/invites?page=0", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['inviteLinks']) == DB_PAGE_SIZE + response = await auth_client.get("/editions/ed2022/invites?page=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['inviteLinks']) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE -def test_create_invite_valid(database_session: Session, auth_client: AuthClient): +async def test_create_invite_valid(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for creating invites when data is valid""" - auth_client.admin() + await auth_client.admin() edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() - # Create POST request - response = auth_client.post("/editions/ed2022/invites/", data=dumps({"email": "test@ema.il"})) - assert response.status_code == status.HTTP_201_CREATED - json = response.json() - assert "mailTo" in json - assert json["mailTo"].startswith("mailto:test@ema.il") - assert "inviteLink" in json + async with auth_client: + # Create POST request + response = await auth_client.post("/editions/ed2022/invites", content=dumps({"email": "test@ema.il"})) + assert response.status_code == status.HTTP_201_CREATED + json = response.json() + assert "mailTo" in json + assert json["mailTo"].startswith("mailto:test@ema.il") + assert "inviteLink" in json - # New entry made in database - json = auth_client.get("/editions/ed2022/invites/").json() - assert len(json["inviteLinks"]) == 1 - new_uuid = json["inviteLinks"][0]["uuid"] - assert auth_client.get(f"/editions/ed2022/invites/{new_uuid}/").status_code == status.HTTP_200_OK + # New entry made in database + json = (await auth_client.get("/editions/ed2022/invites")).json() + assert len(json["inviteLinks"]) == 1 + new_uuid = json["inviteLinks"][0]["uuid"] + assert (await auth_client.get(f"/editions/ed2022/invites/{new_uuid}")).status_code == status.HTTP_200_OK -def test_create_invite_invalid(database_session: Session, auth_client: AuthClient): +async def test_create_invite_invalid(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for creating invites when data is invalid""" - auth_client.admin() + await auth_client.admin() edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() - # Invalid POST will send invalid status code - response = auth_client.post("/editions/ed2022/invites/", data=dumps({"email": "invalid field"})) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + async with auth_client: + # Invalid POST will send invalid status code + response = await auth_client.post("/editions/ed2022/invites", content=dumps({"email": "invalid field"}), follow_redirects=True) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - # Verify that no new entry was made after the error - assert len(auth_client.get("/editions/ed2022/invites/").json()["inviteLinks"]) == 0 + # Verify that no new entry was made after the error + assert len((await auth_client.get("/editions/ed2022/invites", follow_redirects=True)).json()["inviteLinks"]) == 0 -def test_delete_invite_invalid(database_session: Session, auth_client: AuthClient): +async def test_delete_invite_invalid(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for deleting invites when uuid is malformed""" - auth_client.admin() + await auth_client.admin() database_session.add(Edition(year=2022, name="ed2022")) - database_session.commit() + await database_session.commit() - assert auth_client.delete("/editions/ed2022/invites/1").status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + async with auth_client: + assert (await auth_client.delete("/editions/ed2022/invites/1")).status_code == status.HTTP_422_UNPROCESSABLE_ENTITY -def test_delete_invite_valid(database_session: Session, auth_client: AuthClient): +async def test_delete_invite_valid(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for deleting invites when uuid is valid""" - auth_client.admin() + await auth_client.admin() edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() debug_uuid = "123e4567-e89b-12d3-a456-426614174000" - # Not present yet - assert auth_client.delete(f"/editions/ed2022/invites/{debug_uuid}").status_code == status.HTTP_404_NOT_FOUND + async with auth_client: + # Not present yet + assert (await auth_client.delete(f"/editions/ed2022/invites/{debug_uuid}")).status_code == status.HTTP_404_NOT_FOUND - # Create new entry in db - invite_link = InviteLink(target_email="test@ema.il", edition=edition, uuid=UUID(debug_uuid)) - database_session.add(invite_link) - database_session.commit() + # Create new entry in db + invite_link = InviteLink(target_email="test@ema.il", edition=edition, uuid=UUID(debug_uuid)) + database_session.add(invite_link) + await database_session.commit() - # Remove - assert auth_client.delete(f"/editions/ed2022/invites/{invite_link.uuid}").status_code == status.HTTP_204_NO_CONTENT + # Remove + assert (await auth_client.delete(f"/editions/ed2022/invites/{invite_link.uuid}")).status_code == status.HTTP_204_NO_CONTENT - # Not found anymore - assert auth_client.get(f"/editions/ed2022/invites/{invite_link.uuid}/").status_code == status.HTTP_404_NOT_FOUND + # Not found anymore + assert (await auth_client.get(f"/editions/ed2022/invites/{invite_link.uuid}")).status_code == status.HTTP_404_NOT_FOUND -def test_get_invite_malformed_uuid(database_session: Session, auth_client: AuthClient): +async def test_get_invite_malformed_uuid(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for fetching invites when uuid is malformed""" - auth_client.admin() + await auth_client.admin() edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() - # Verify malformed uuid (1) - assert auth_client.get("/editions/ed2022/invites/1/").status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + async with auth_client: + # Verify malformed uuid (1) + assert (await auth_client.get("/editions/ed2022/invites/1")).status_code == status.HTTP_422_UNPROCESSABLE_ENTITY -def test_get_invite_non_existing(database_session: Session, auth_client: AuthClient): +async def test_get_invite_non_existing(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for fetching invites when uuid is valid but doesn't exist""" - auth_client.admin() + await auth_client.admin() edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() - assert auth_client.get( - "/editions/ed2022/invites/123e4567-e89b-12d3-a456-426614174000").status_code == status.HTTP_404_NOT_FOUND + async with auth_client: + assert (await auth_client.get("/editions/ed2022/invites/123e4567-e89b-12d3-a456-426614174000"))\ + .status_code == status.HTTP_404_NOT_FOUND -def test_get_invite_present(database_session: Session, auth_client: AuthClient): +async def test_get_invite_present(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint to fetch an invite when one is present""" - auth_client.admin() + await auth_client.admin() edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() debug_uuid = "123e4567-e89b-12d3-a456-426614174000" # Create new entry in db invite_link = InviteLink(target_email="test@ema.il", edition=edition, uuid=UUID(debug_uuid)) database_session.add(invite_link) - database_session.commit() + await database_session.commit() - # Found the correct result now - response = auth_client.get(f"/editions/ed2022/invites/{debug_uuid}") + async with auth_client: + # Found the correct result now + response = await auth_client.get(f"/editions/ed2022/invites/{debug_uuid}") json = response.json() assert response.status_code == status.HTTP_200_OK assert json["uuid"] == debug_uuid assert json["email"] == "test@ema.il" -def test_create_invite_valid_old_edition(database_session: Session, auth_client: AuthClient): +async def test_create_invite_valid_readonly_edition(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for creating invites when data is valid, but the edition is read-only""" - auth_client.admin() - edition = Edition(year=2022, name="ed2022") + await auth_client.admin() + edition = Edition(year=2022, name="ed2022", readonly=True) edition2 = Edition(year=2023, name="ed2023") database_session.add(edition) database_session.add(edition2) - database_session.commit() + await database_session.commit() - # Create POST request - response = auth_client.post("/editions/ed2022/invites/", data=dumps({"email": "test@ema.il"})) + async with auth_client: + # Create POST request + response = await auth_client.post("/editions/ed2022/invites", content=dumps({"email": "test@ema.il"})) assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED diff --git a/backend/tests/test_routers/test_editions/test_projects/test_projects.py b/backend/tests/test_routers/test_editions/test_projects/test_projects.py index 556604c90..ad330a011 100644 --- a/backend/tests/test_routers/test_editions/test_projects/test_projects.py +++ b/backend/tests/test_routers/test_editions/test_projects/test_projects.py @@ -1,333 +1,645 @@ import pytest -from sqlalchemy.orm import Session +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status from settings import DB_PAGE_SIZE -from src.database.models import Edition, Project, User, Skill, ProjectRole, Student, Partner +from src.database.models import Edition, Project, Skill, User, Partner from tests.utils.authorization import AuthClient -@pytest.fixture -def database_with_data(database_session: Session) -> Session: - """fixture for adding data to the database""" - edition: Edition = Edition(year=2022, name="ed2022") - database_session.add(edition) - project1 = Project(name="project1", edition=edition, number_of_students=2) - project2 = Project(name="project2", edition=edition, number_of_students=3) - project3 = Project(name="super nice project", edition=edition, number_of_students=3) - database_session.add(project1) - database_session.add(project2) - database_session.add(project3) - user: User = User(name="coach1") - database_session.add(user) - skill1: Skill = Skill(name="skill1", description="something about skill1") - skill2: Skill = Skill(name="skill2", description="something about skill2") - skill3: Skill = Skill(name="skill3", description="something about skill3") - database_session.add(skill1) - database_session.add(skill2) - database_session.add(skill3) - student01: Student = Student(first_name="Jos", last_name="Vermeulen", preferred_name="Joske", - email_address="josvermeulen@mail.com", phone_number="0487/86.24.45", alumni=True, - wants_to_be_student_coach=True, edition=edition, skills=[skill1, skill3]) - student02: Student = Student(first_name="Isabella", last_name="Christensen", preferred_name="Isabella", - email_address="isabella.christensen@example.com", phone_number="98389723", alumni=True, - wants_to_be_student_coach=True, edition=edition, skills=[skill2]) - project_role1: ProjectRole = ProjectRole( - student=student01, project=project1, skill=skill1, drafter=user, argumentation="argmunet") - project_role2: ProjectRole = ProjectRole( - student=student01, project=project2, skill=skill3, drafter=user, argumentation="argmunet") - project_role3: ProjectRole = ProjectRole( - student=student02, project=project1, skill=skill1, drafter=user, argumentation="argmunet") - database_session.add(project_role1) - database_session.add(project_role2) - database_session.add(project_role3) - database_session.commit() - - return database_session - - -@pytest.fixture -def current_edition(database_with_data: Session) -> Edition: - """fixture to get the latest edition""" - return database_with_data.query(Edition).all()[-1] - - -def test_get_projects(database_with_data: Session, auth_client: AuthClient): - """Tests get all projects""" - auth_client.admin() - response = auth_client.get("/editions/ed2022/projects") - json = response.json() - - assert len(json['projects']) == 3 - assert json['projects'][0]['name'] == "project1" - assert json['projects'][1]['name'] == "project2" - assert json['projects'][2]['name'] == "super nice project" - - -def test_get_projects_paginated(database_session: Session, auth_client: AuthClient): +async def test_get_projects_paginated(database_session: AsyncSession, auth_client: AuthClient): """test get all projects paginated""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) for i in range(round(DB_PAGE_SIZE * 1.5)): - database_session.add(Project(name=f"Project {i}", edition=edition, number_of_students=5)) - database_session.commit() + database_session.add(Project(name=f"Project {i}", edition=edition)) + await database_session.commit() - auth_client.admin() + await auth_client.admin() + async with auth_client: + response = await auth_client.get("/editions/ed2022/projects?page=0", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['projects']) == DB_PAGE_SIZE + response = await auth_client.get("/editions/ed2022/projects?page=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['projects']) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE - response = auth_client.get("/editions/ed2022/projects?page=0") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['projects']) == DB_PAGE_SIZE - response = auth_client.get("/editions/ed2022/projects?page=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['projects']) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE - -def test_get_project(database_with_data: Session, auth_client: AuthClient): +async def test_get_project(database_session: AsyncSession, auth_client: AuthClient): """Tests get a specific project""" - auth_client.admin() - response = auth_client.get("/editions/ed2022/projects/1") - assert response.status_code == status.HTTP_200_OK - json = response.json() - assert json['name'] == 'project1' + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(edition=edition, name="project 1") + database_session.add(project) + await database_session.commit() + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get(f"/editions/{edition.name}/projects/{project.project_id}") + assert response.status_code == status.HTTP_200_OK + json = response.json() + assert json['projectId'] == project.project_id + assert json['name'] == project.name + assert len(json['coaches']) == 0 + assert len(json['partners']) == 0 + assert len(json['projectRoles']) == 0 -def test_delete_project(database_with_data: Session, auth_client: AuthClient): + +async def test_delete_project(database_session: AsyncSession, auth_client: AuthClient): """Tests delete a project""" - auth_client.admin() - response = auth_client.get("/editions/ed2022/projects/1") - assert response.status_code == status.HTTP_200_OK - response = auth_client.delete("/editions/ed2022/projects/1") - assert response.status_code == status.HTTP_204_NO_CONTENT - response = auth_client.get("/editions/ed2022/projects/1") - assert response.status_code == status.HTTP_404_NOT_FOUND + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(edition=edition, name="project 1") + database_session.add(project) + await database_session.commit() + await auth_client.admin() + endpoint = f"/editions/{edition.name}/projects/{project.project_id}" -def test_delete_ghost_project(database_with_data: Session, auth_client: AuthClient): - """Tests delete a project that doesn't exist""" - auth_client.admin() - response = auth_client.get("/editions/ed2022/projects/400") - assert response.status_code == status.HTTP_404_NOT_FOUND - response = auth_client.delete("/editions/ed2022/projects/400") - assert response.status_code == status.HTTP_404_NOT_FOUND + async with auth_client: + response = await auth_client.get(endpoint) + assert response.status_code == status.HTTP_200_OK + response = await auth_client.delete(endpoint) + assert response.status_code == status.HTTP_204_NO_CONTENT + response = await auth_client.get(endpoint) + assert response.status_code == status.HTTP_404_NOT_FOUND -def test_create_project(database_with_data: Session, auth_client: AuthClient): - """Tests creating a project""" - auth_client.admin() - response = auth_client.get('/editions/ed2022/projects') - json = response.json() - assert len(json['projects']) == 3 - assert len(database_with_data.query(Partner).all()) == 0 +async def test_delete_project_not_found(database_session: AsyncSession, auth_client: AuthClient): + """Tests delete a project that doesn't exist""" + edition: Edition = Edition(year=2022, name="ed2022") + database_session.add(edition) + await database_session.commit() - response = \ - auth_client.post("/editions/ed2022/projects/", - json={"name": "test", - "number_of_students": 5, - "skills": [1, 1, 1, 1, 1], "partners": ["ugent"], "coaches": [1]}) + await auth_client.admin() + async with auth_client: + response = await auth_client.delete(f"/editions/{edition.name}/projects/400") + assert response.status_code == status.HTTP_404_NOT_FOUND - assert response.status_code == status.HTTP_201_CREATED - assert response.json()['name'] == 'test' - assert response.json()["partners"][0]["name"] == "ugent" - assert len(database_with_data.query(Partner).all()) == 1 +async def test_create_project(database_session: AsyncSession, auth_client: AuthClient): + """Tests creating a project""" + edition: Edition = Edition(year=2022, name="ed2022") + user: User = User(name="coach 1") + database_session.add(edition) + database_session.add(user) + await database_session.commit() + + await auth_client.admin() - response = auth_client.get('/editions/ed2022/projects') - json = response.json() + async with auth_client: + response = await auth_client.post("/editions/ed2022/projects", json={ + "name": "test", + "info_url": "https://info.com", + "partners": ["ugent"], + "coaches": [user.user_id] + }) - assert len(json['projects']) == 4 - assert json['projects'][3]['name'] == "test" + assert response.status_code == status.HTTP_201_CREATED + json: dict = response.json() + assert "projectId" in json + assert json["name"] == "test" + assert json["infoUrl"] == "https://info.com" + assert json["partners"][0]["name"] == "ugent" + assert json["coaches"][0]["name"] == user.name + assert len(json["projectRoles"]) == 0 -def test_create_project_same_partner(database_with_data: Session, auth_client: AuthClient): +async def test_create_project_same_partner(database_session: AsyncSession, auth_client: AuthClient): """Tests that creating a project doesn't create a partner if the partner already exists""" - auth_client.admin() - assert len(database_with_data.query(Partner).all()) == 0 + edition: Edition = Edition(year=2022, name="ed2022") + user: User = User(name="coach 1") + database_session.add(edition) + database_session.add(user) + await database_session.commit() + + assert len((await database_session.execute(select(Partner))).unique().scalars().all()) == 0 + + await auth_client.admin() + async with auth_client: + + await auth_client.post(f"/editions/{edition.name}/projects", json={ + "name": "test", + "info_url": "https://info.com", + "partners": ["ugent"], + "coaches": [user.user_id] + }) + await auth_client.post(f"/editions/{edition.name}/projects", json={ + "name": "test", + "info_url": "https://info.com", + "partners": ["ugent"], + "coaches": [user.user_id] + }) + assert len((await database_session.execute(select(Partner))).unique().scalars().all()) == 1 + + +@pytest.mark.skip(reason="The async database rolls back everything, even with nested query") +async def test_create_project_non_existing_coach(database_session: AsyncSession, auth_client: AuthClient): + """Tests creating a project with a coach that doesn't exist""" + edition: Edition = Edition(year=2022, name="ed2022") + database_session.add(edition) + await database_session.commit() + + await auth_client.admin() + endpoint = f"/editions/{edition.name}/projects" - auth_client.post("/editions/ed2022/projects/", - json={"name": "test1", - "number_of_students": 2, - "skills": [1, 2], "partners": ["ugent"], "coaches": [1]}) - auth_client.post("/editions/ed2022/projects/", - json={"name": "test2", - "number_of_students": 2, - "skills": [1, 2], "partners": ["ugent"], "coaches": [1]}) - assert len(database_with_data.query(Partner).all()) == 1 + await database_session.begin_nested() + async with auth_client: + response = await auth_client.post(endpoint, json={ + "name": "test", + "info_url": "https://info.com", + "partners": ["ugent"], + "coaches": [0] + }) + assert response.status_code == status.HTTP_404_NOT_FOUND + response = await auth_client.get(f"/editions/{edition.name}/projects/") + assert len(response.json()['projects']) == 0 -def test_create_project_non_existing_skills(database_with_data: Session, auth_client: AuthClient): - """Tests creating a project with non-existing skills""" - auth_client.admin() - response = auth_client.get('/editions/ed2022/projects') - json = response.json() - assert len(json['projects']) == 3 +async def test_create_project_no_name(database_session: AsyncSession, auth_client: AuthClient): + """Tests creating a project that has no name""" + edition: Edition = Edition(year=2022, name="ed2022") + database_session.add(edition) + await database_session.commit() - assert len(database_with_data.query(Skill).where( - Skill.skill_id == 100).all()) == 0 + await auth_client.admin() - response = auth_client.post("/editions/ed2022/projects/", - json={"name": "test1", - "number_of_students": 1, - "skills": [100], "partners": ["ugent"], "coaches": [1]}) - assert response.status_code == status.HTTP_404_NOT_FOUND + await database_session.begin_nested() + async with auth_client: + response = await auth_client.post(f"/editions/{edition.name}/projects", json={ + "info_url": "https://info.com", + "partners": [], + "coaches": [] + }) - response = auth_client.get('/editions/ed2022/projects') - json = response.json() - assert len(json['projects']) == 3 + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + response = await auth_client.get(f"/editions/{edition.name}/projects/", follow_redirects=True) + assert len(response.json()['projects']) == 0 -def test_create_project_non_existing_coach(database_with_data: Session, auth_client: AuthClient): - """Tests creating a project with a coach that doesn't exist""" - auth_client.admin() - response = auth_client.get('/editions/ed2022/projects') - json = response.json() - assert len(json['projects']) == 3 +async def test_create_project_no_input_url(database_session: AsyncSession, auth_client: AuthClient): + """Tests creating a project that has no name""" + edition: Edition = Edition(year=2022, name="ed2022") + database_session.add(edition) + await database_session.commit() - assert len(database_with_data.query(Student).where( - Student.edition_id == 10).all()) == 0 + await auth_client.admin() - response = auth_client.post("/editions/ed2022/projects/", - json={"name": "test2", - "number_of_students": 1, - "skills": [100], "partners": ["ugent"], "coaches": [10]}) - assert response.status_code == status.HTTP_404_NOT_FOUND + await database_session.begin_nested() + async with auth_client: + response = await auth_client.post(f"/editions/{edition.name}/projects", json={ + "name": "test", + "partners": [], + "coaches": [] + }) - response = auth_client.get('/editions/ed2022/projects') - json = response.json() - assert len(json['projects']) == 3 + assert response.status_code == status.HTTP_201_CREATED + json: dict = response.json() + assert "projectId" in json + assert json["name"] == "test" + assert json["infoUrl"] is None + assert len(json["partners"]) == 0 + assert len(json["coaches"]) == 0 + assert len(json["projectRoles"]) == 0 -def test_create_project_no_name(database_with_data: Session, auth_client: AuthClient): +async def test_create_project_invalid_input_url(database_session: AsyncSession, auth_client: AuthClient): """Tests creating a project that has no name""" - auth_client.admin() - response = auth_client.get('/editions/ed2022/projects') - json = response.json() - assert len(json['projects']) == 3 - response = \ - auth_client.post("/editions/ed2022/projects/", - # project has no name - json={ - "number_of_students": 5, - "skills": [], "partners": [], "coaches": []}) - - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + edition: Edition = Edition(year=2022, name="ed2022") + database_session.add(edition) + await database_session.commit() - response = auth_client.get('/editions/ed2022/projects') - json = response.json() - assert len(json['projects']) == 3 + await auth_client.admin() + await database_session.begin_nested() + async with auth_client: + response = await auth_client.post(f"/editions/{edition.name}/projects", json={ + "name": "test", + "info_url": "ssh://info.com", + "partners": [], + "coaches": [] + }) -def test_patch_project(database_with_data: Session, auth_client: AuthClient): - """Tests patching a project""" - auth_client.admin() - response = auth_client.get('/editions/ed2022/projects') - json = response.json() + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - assert len(json['projects']) == 3 + response = await auth_client.get(f"/editions/{edition.name}/projects/", follow_redirects=True) + assert len(response.json()['projects']) == 0 - response = auth_client.patch("/editions/ed2022/projects/1", - json={"name": "patched", - "number_of_students": 5, - "skills": [1, 1, 1, 1, 1], "partners": ["ugent"], "coaches": [1]}) - assert response.status_code == status.HTTP_204_NO_CONTENT - response = auth_client.get('/editions/ed2022/projects') - json = response.json() +async def test_patch_project(database_session: AsyncSession, auth_client: AuthClient): + """Tests patching a project""" + edition: Edition = Edition(year=2022, name="ed2022") + partner: Partner = Partner(name="partner 1") + user: User = User(name="user 1") + project: Project = Project(name="project 1", edition=edition, partners=[ + partner], coaches=[user]) + database_session.add(project) + await database_session.commit() - assert len(json['projects']) == 3 - assert json['projects'][0]['name'] == 'patched' + await auth_client.admin() + new_user: User = User(name="new user") + database_session.add(new_user) + await database_session.commit() -def test_patch_project_non_existing_skills(database_with_data: Session, auth_client: AuthClient): - """Tests patching a project with non-existing skills""" - auth_client.admin() - assert len(database_with_data.query(Skill).where( - Skill.skill_id == 100).all()) == 0 + async with auth_client: + response = await auth_client.patch(f"/editions/{edition.name}/projects/{project.project_id}", json={ + "name": "patched", + "partners": ["ugent"], + "coaches": [new_user.user_id]}) + assert response.status_code == status.HTTP_204_NO_CONTENT - response = auth_client.patch("/editions/ed2022/projects/1", - json={"name": "test1", - "number_of_students": 1, - "skills": [100], "partners": ["ugent"], "coaches": [1]}) - assert response.status_code == status.HTTP_404_NOT_FOUND + response = await auth_client.get(f'/editions/{edition.name}/projects') + json = response.json() - response = auth_client.get("/editions/ed2022/projects/1") - json = response.json() - assert 100 not in json["skills"] + assert len(json['projects']) == 1 + assert json['projects'][0]['name'] == 'patched' + assert len(json['projects'][0]['partners']) == 1 + assert json['projects'][0]['partners'][0]['name'] == 'ugent' + assert len(json['projects'][0]['coaches']) == 1 + assert json['projects'][0]['coaches'][0]['name'] == new_user.name -def test_patch_project_non_existing_coach(database_with_data: Session, auth_client: AuthClient): +@pytest.mark.skip(reason="The async database rolls back everything, even with nested query") +async def test_patch_project_non_existing_coach(database_session: AsyncSession, auth_client: AuthClient): """Tests patching a project with a coach that doesn't exist""" - auth_client.admin() - assert len(database_with_data.query(Student).where( - Student.edition_id == 10).all()) == 0 + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project 1", edition=edition) + database_session.add(project) + await database_session.commit() + + await auth_client.admin() - response = auth_client.patch("/editions/ed2022/projects/1", - json={"name": "test2", - "number_of_students": 1, - "skills": [100], "partners": ["ugent"], "coaches": [10]}) - assert response.status_code == status.HTTP_404_NOT_FOUND - response = auth_client.get("/editions/ed2022/projects/1") - json = response.json() - assert 10 not in json["coaches"] + await database_session.begin_nested() + async with auth_client: + response = await auth_client.patch(f"/editions/{edition.name}/projects/{project.project_id}", json={ + "name": "test2", + "partners": [], + "coaches": [10] + }) + assert response.status_code == status.HTTP_404_NOT_FOUND + response = await auth_client.get(f'/editions/{edition.name}/projects/{project.project_id}') + assert len(response.json()['coaches']) == 0 -def test_patch_wrong_project(database_session: Session, auth_client: AuthClient): + +async def test_patch_wrong_project(database_session: AsyncSession, auth_client: AuthClient): """Tests patching with wrong project info""" - auth_client.admin() - database_session.add(Edition(year=2022, name="ed2022")) - project = Project(name="project", edition_id=1, - project_id=1, number_of_students=2) + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project 1", edition=edition) database_session.add(project) - database_session.commit() + await database_session.commit() - response = \ - auth_client.patch("/editions/ed2022/projects/1", - json={"name": "patched", - "skills": [], "partners": [], "coaches": []}) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + await auth_client.admin() - response2 = auth_client.get('/editions/ed2022/projects') - json = response2.json() + await database_session.begin_nested() + async with auth_client: + response = await auth_client.patch(f"/editions/{edition.name}/projects/{project.project_id}", json={ + "name": "patched", + "partners": [] + }) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - assert len(json['projects']) == 1 - assert json['projects'][0]['name'] == 'project' + response = await auth_client.get(f'/editions/{edition.name}/projects/', follow_redirects=True) + json = response.json() + assert len(json['projects']) == 1 + assert json['projects'][0]['name'] == project.name -def test_create_project_old_edition(database_with_data: Session, auth_client: AuthClient): +async def test_create_project_readonly_edition(database_session: AsyncSession, auth_client: AuthClient): """test create a project for a readonly edition""" - auth_client.admin() - database_with_data.add(Edition(year=2023, name="ed2023")) - database_with_data.commit() + edition_22: Edition = Edition(year=2022, name="ed2022", readonly=True) + edition_23: Edition = Edition(year=2023, name="ed2023") + database_session.add(edition_22) + database_session.add(edition_23) + await database_session.commit() - response = \ - auth_client.post("/editions/ed2022/projects/", - json={"name": "test", - "number_of_students": 5, - "skills": [1, 1, 1, 1, 1], "partners": ["ugent"], "coaches": [1]}) + await auth_client.admin() - assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + async with auth_client: + response = await auth_client.post(f"/editions/{edition_22.name}/projects", json={ + "name": "test", + "partners": ["ugent"], + "coaches": [] + }) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED -def test_search_project_name(database_with_data: Session, auth_client: AuthClient): +async def test_search_project_name(database_session: AsyncSession, auth_client: AuthClient): """test search project on name""" - auth_client.admin() - response = auth_client.get("/editions/ed2022/projects/?name=super") - assert len(response.json()["projects"]) == 1 - assert response.json()["projects"][0]["name"] == "super nice project" + edition: Edition = Edition(year=2022, name="ed2022") + database_session.add(Project(name="project 1", edition=edition)) + database_session.add(Project(name="project 2", edition=edition)) + await database_session.commit() + + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get(f"/editions/{edition.name}/projects?name=1") + assert len(response.json()["projects"]) == 1 + assert response.json()["projects"][0]["name"] == "project 1" -def test_search_project_coach(database_with_data: Session, auth_client: AuthClient): +async def test_search_project_coach(database_session: AsyncSession, auth_client: AuthClient): """test search project on coach""" - auth_client.admin() - user: User = database_with_data.query(User).where(User.name == "Pytest Admin").one() - auth_client.post("/editions/ed2022/projects/", - json={"name": "test", - "number_of_students": 2, - "skills": [1, 1, 1, 1, 1], "partners": ["ugent"], "coaches": [user.user_id]}) - response = auth_client.get("/editions/ed2022/projects/?coach=true") - print(response.json()) - assert len(response.json()["projects"]) == 1 - assert response.json()["projects"][0]["name"] == "test" - assert response.json()["projects"][0]["coaches"][0]["userId"] == user.user_id + edition: Edition = Edition(year=2022, name="ed2022") + + await auth_client.coach(edition) + + database_session.add(Project(name="project 1", edition=edition)) + database_session.add( + Project(name="project 2", edition=edition, coaches=[auth_client.user])) + await database_session.commit() + + async with auth_client: + response = await auth_client.get(f"/editions/{edition.name}/projects?coach=true", follow_redirects=True) + json = response.json() + assert len(json["projects"]) == 1 + assert json["projects"][0]["name"] == "project 2" + assert json["projects"][0]["coaches"][0]["userId"] == auth_client.user.user_id + + +async def test_delete_project_role(database_session: AsyncSession, auth_client: AuthClient): + """test delete a project role""" + edition: Edition = Edition(year=2022, name="ed2022") + user: User = User(name="coach 1") + skill: Skill = Skill(name="Skill1") + database_session.add(edition) + database_session.add(user) + database_session.add(skill) + await database_session.commit() + + await auth_client.admin() + + async with auth_client: + response = await auth_client.post("/editions/ed2022/projects", json={ + "name": "test", + "partners": ["ugent"], + "coaches": [user.user_id] + }) + + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["projectId"] == 1 + response = await auth_client.post("/editions/ed2022/projects/1/roles", json={ + "skill_id": 1, + "description": "description", + "slots": 1 + }) + response = await auth_client.get("/editions/ed2022/projects/1/roles") + assert len(response.json()["projectRoles"]) == 1 + response = await auth_client.delete("/editions/ed2022/projects/1/roles/1") + assert response.status_code == status.HTTP_204_NO_CONTENT + response = await auth_client.get("/editions/ed2022/projects/1/roles") + assert len(response.json()["projectRoles"]) == 0 + + +async def test_make_project_role(database_session: AsyncSession, auth_client: AuthClient): + """test make a project role""" + edition: Edition = Edition(year=2022, name="ed2022") + user: User = User(name="coach 1") + skill: Skill = Skill(name="Skill1") + database_session.add(edition) + database_session.add(user) + database_session.add(skill) + await database_session.commit() + + await auth_client.admin() + + async with auth_client: + response = await auth_client.post("/editions/ed2022/projects", json={ + "name": "test", + "partners": ["ugent"], + "coaches": [user.user_id] + }) + + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["projectId"] == 1 + response = await auth_client.post("/editions/ed2022/projects/1/roles", json={ + "skill_id": 1, + "description": "description", + "slots": 1 + }) + assert response.status_code == status.HTTP_201_CREATED + json = response.json() + assert json["projectRoleId"] == 1 + assert json["projectId"] == 1 + assert json["description"] == "description" + assert json["skill"]["skillId"] == 1 + assert json["slots"] == 1 + + +async def test_make_project_role_negative_slots(database_session: AsyncSession, auth_client: AuthClient): + """test make a project role""" + edition: Edition = Edition(year=2022, name="ed2022") + user: User = User(name="coach 1") + skill: Skill = Skill(name="Skill1") + database_session.add(edition) + database_session.add(user) + database_session.add(skill) + await database_session.commit() + + await auth_client.admin() + + async with auth_client: + response = await auth_client.post("/editions/ed2022/projects", json={ + "name": "test", + "partners": ["ugent"], + "coaches": [user.user_id] + }) + + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["projectId"] == 1 + response = await auth_client.post("/editions/ed2022/projects/1/roles", json={ + "skill_id": 1, + "description": "description", + "slots": -1 + }) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + +async def test_make_project_role_zero_slots(database_session: AsyncSession, auth_client: AuthClient): + """test make a project role""" + edition: Edition = Edition(year=2022, name="ed2022") + user: User = User(name="coach 1") + skill: Skill = Skill(name="Skill1") + database_session.add(edition) + database_session.add(user) + database_session.add(skill) + await database_session.commit() + + await auth_client.admin() + + async with auth_client: + response = await auth_client.post("/editions/ed2022/projects", json={ + "name": "test", + "partners": ["ugent"], + "coaches": [user.user_id] + }) + + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["projectId"] == 1 + response = await auth_client.post("/editions/ed2022/projects/1/roles", json={ + "skill_id": 1, + "description": "description", + "slots": 0 + }) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + +async def test_update_project_role(database_session: AsyncSession, auth_client: AuthClient): + """test update a project role""" + edition: Edition = Edition(year=2022, name="ed2022") + user: User = User(name="coach 1") + skill: Skill = Skill(name="Skill1") + database_session.add(edition) + database_session.add(user) + database_session.add(skill) + await database_session.commit() + + await auth_client.admin() + + async with auth_client: + response = await auth_client.post("/editions/ed2022/projects", json={ + "name": "test", + "partners": ["ugent"], + "coaches": [user.user_id] + }) + + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["projectId"] == 1 + response = await auth_client.post("/editions/ed2022/projects/1/roles", json={ + "skill_id": 1, + "description": "description", + "slots": 1 + }) + assert response.status_code == status.HTTP_201_CREATED + response = await auth_client.patch("/editions/ed2022/projects/1/roles/1", json={ + "skill_id": 1, + "description": "changed", + "slots": 2 + }) + assert response.status_code == status.HTTP_200_OK + json = response.json() + assert json["projectRoleId"] == 1 + assert json["projectId"] == 1 + assert json["description"] == "changed" + assert json["skill"]["skillId"] == 1 + assert json["slots"] == 2 + response = await auth_client.get("/editions/ed2022/projects/1/roles") + assert response.status_code == status.HTTP_200_OK + json = response.json() + assert len(json["projectRoles"]) == 1 + assert json["projectRoles"][0]["projectRoleId"] == 1 + assert json["projectRoles"][0]["projectId"] == 1 + assert json["projectRoles"][0]["description"] == "changed" + assert json["projectRoles"][0]["skill"]["skillId"] == 1 + assert json["projectRoles"][0]["slots"] == 2 + + +async def test_update_project_role_negative_slots(database_session: AsyncSession, auth_client: AuthClient): + """test update a project role with negative slots""" + edition: Edition = Edition(year=2022, name="ed2022") + user: User = User(name="coach 1") + skill: Skill = Skill(name="Skill1") + database_session.add(edition) + database_session.add(user) + database_session.add(skill) + await database_session.commit() + + await auth_client.admin() + + async with auth_client: + response = await auth_client.post("/editions/ed2022/projects", json={ + "name": "test", + "partners": ["ugent"], + "coaches": [user.user_id] + }) + + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["projectId"] == 1 + response = await auth_client.post("/editions/ed2022/projects/1/roles", json={ + "skill_id": 1, + "description": "description", + "slots": 1 + }) + assert response.status_code == status.HTTP_201_CREATED + response = await auth_client.patch("/editions/ed2022/projects/1/roles/1", json={ + "skill_id": 1, + "description": "description", + "slots": -1 + }) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + +async def test_update_project_role_zero_slots(database_session: AsyncSession, auth_client: AuthClient): + """test update a project role with zero slots""" + edition: Edition = Edition(year=2022, name="ed2022") + user: User = User(name="coach 1") + skill: Skill = Skill(name="Skill1") + database_session.add(edition) + database_session.add(user) + database_session.add(skill) + await database_session.commit() + + await auth_client.admin() + + async with auth_client: + response = await auth_client.post("/editions/ed2022/projects", json={ + "name": "test", + "partners": ["ugent"], + "coaches": [user.user_id] + }) + + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["projectId"] == 1 + response = await auth_client.post("/editions/ed2022/projects/1/roles", json={ + "skill_id": 1, + "description": "description", + "slots": 1 + }) + assert response.status_code == status.HTTP_201_CREATED + response = await auth_client.patch("/editions/ed2022/projects/1/roles/1", json={ + "skill_id": 1, + "description": "description", + "slots": 0 + }) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + +async def test_get_project_role(database_session: AsyncSession, auth_client: AuthClient): + """test get project role""" + edition: Edition = Edition(year=2022, name="ed2022") + user: User = User(name="coach 1") + skill: Skill = Skill(name="Skill1") + database_session.add(edition) + database_session.add(user) + database_session.add(skill) + await database_session.commit() + + await auth_client.admin() + + async with auth_client: + response = await auth_client.post("/editions/ed2022/projects", json={ + "name": "test", + "partners": ["ugent"], + "coaches": [user.user_id] + }) + + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["projectId"] == 1 + response = await auth_client.post("/editions/ed2022/projects/1/roles", json={ + "skill_id": 1, + "description": "description", + "slots": 1 + }) + assert response.status_code == status.HTTP_201_CREATED + response = await auth_client.get("/editions/ed2022/projects/1/roles") + assert response.status_code == status.HTTP_200_OK + json = response.json() + assert len(json["projectRoles"]) == 1 + assert json["projectRoles"][0]["projectRoleId"] == 1 + assert json["projectRoles"][0]["projectId"] == 1 + assert json["projectRoles"][0]["description"] == "description" + assert json["projectRoles"][0]["skill"]["skillId"] == 1 + assert json["projectRoles"][0]["slots"] == 1 diff --git a/backend/tests/test_routers/test_editions/test_projects/test_students/test_students.py b/backend/tests/test_routers/test_editions/test_projects/test_students/test_students.py index a2c431e61..2476a2938 100644 --- a/backend/tests/test_routers/test_editions/test_projects/test_students/test_students.py +++ b/backend/tests/test_routers/test_editions/test_projects/test_students/test_students.py @@ -1,380 +1,409 @@ -import pytest -from fastapi.testclient import TestClient -from sqlalchemy.orm import Session +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status -from src.database.models import Edition, Project, User, Skill, ProjectRole, Student +from src.database.models import Edition, Project, Skill, ProjectRole, Student, ProjectRoleSuggestion from tests.utils.authorization import AuthClient -@pytest.fixture -def database_with_data(database_session: Session) -> Session: - """fixture for adding data to the database""" +async def test_add_pr_suggestion(database_session: AsyncSession, auth_client: AuthClient): + """tests add a student to a project""" edition: Edition = Edition(year=2022, name="ed2022") - database_session.add(edition) - skill1: Skill = Skill(name="skill1", description="something about skill1") - skill2: Skill = Skill(name="skill2", description="something about skill2") - skill3: Skill = Skill(name="skill3", description="something about skill3") - skill4: Skill = Skill(name="skill4", description="something about skill4") - skill5: Skill = Skill(name="skill5", description="something about skill5") - database_session.add(skill1) - database_session.add(skill2) - database_session.add(skill3) - database_session.add(skill4) - database_session.add(skill5) - project1 = Project(name="project1", edition=edition, number_of_students=4, skills=[skill1, skill2, skill3, skill4, skill5]) - project2 = Project(name="project2", edition=edition, number_of_students=3, skills=[skill1, skill2, skill3, skill4]) - project3 = Project(name="project3", edition=edition, number_of_students=3, skills=[skill1, skill2, skill3]) - database_session.add(project1) - database_session.add(project2) - database_session.add(project3) - user: User = User(name="coach1") - database_session.add(user) - student01: Student = Student(first_name="Jos", last_name="Vermeulen", preferred_name="Joske", - email_address="josvermeulen@mail.com", phone_number="0487/86.24.45", alumni=True, - wants_to_be_student_coach=True, edition=edition, skills=[skill1, skill3, skill4]) - student02: Student = Student(first_name="Isabella", last_name="Christensen", preferred_name="Isabella", - email_address="isabella.christensen@example.com", phone_number="98389723", alumni=True, - wants_to_be_student_coach=True, edition=edition, skills=[skill2, skill4]) - student03: Student = Student(first_name="Lotte", last_name="Buss", preferred_name="Lotte", - email_address="lotte.buss@example.com", phone_number="0284-0749932", alumni=False, - wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill3, skill4]) - student04: Student = Student(first_name="Max", last_name="Tester", preferred_name="Mxa", - email_address="max.test@example.com", phone_number="0284-1356832", alumni=False, - wants_to_be_student_coach=False, edition=edition, skills=[skill5]) - database_session.add(student01) - database_session.add(student02) - database_session.add(student03) - database_session.add(student04) - project_role1: ProjectRole = ProjectRole( - student=student01, project=project1, skill=skill1, drafter=user, argumentation="argmunet") - project_role2: ProjectRole = ProjectRole( - student=student01, project=project2, skill=skill3, drafter=user, argumentation="argmunet") - project_role3: ProjectRole = ProjectRole( - student=student02, project=project1, skill=skill2, drafter=user, argumentation="argmunet") - project_role4: ProjectRole = ProjectRole( - student=student04, project=project1, skill=skill5, drafter=user, argumentation="argmunet", definitive=True) - database_session.add(project_role1) - database_session.add(project_role2) - database_session.add(project_role3) - database_session.add(project_role4) - database_session.commit() - - return database_session - - -@pytest.fixture -def current_edition(database_with_data: Session) -> Edition: - """fixture to get the latest edition""" - return database_with_data.query(Edition).all()[-1] - - -def test_add_student_project(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): + project: Project = Project(name="project 1", edition=edition) + skill: Skill = Skill(name="skill 1") + project_role: ProjectRole = ProjectRole(project=project, skill=skill, slots=1) + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, + edition=edition + ) + database_session.add(project_role) + database_session.add(student) + await database_session.commit() + + await auth_client.coach(edition) + async with auth_client: + resp = await auth_client.post( + f"/editions/{edition.name}/projects/{project.project_id}/roles/{project_role.project_role_id}/students/{student.student_id}", + json={"argumentation": "argumentation"} + ) + assert resp.status_code == status.HTTP_201_CREATED + json1 = resp.json() + assert json1["projectRoleSuggestion"]["projectRoleSuggestionId"] == 1 + assert json1["projectRoleSuggestion"]["argumentation"] == 'argumentation' + response2 = await auth_client.get(f'/editions/{edition.name}/projects/{project.project_id}') + json2 = response2.json() + assert len(json2['projectRoles']) == 1 + assert len(json2['projectRoles'][0]['suggestions']) == 1 + assert json2['projectRoles'][0]['suggestions'][0]['argumentation'] == 'argumentation' + + +async def test_add_pr_suggestion_duplicate(database_session: AsyncSession, auth_client: AuthClient): """tests add a student to a project""" - auth_client.coach(current_edition) - - resp = auth_client.post( - "/editions/ed2022/projects/1/students/3", json={"skill_id": 3}) - - assert resp.status_code == status.HTTP_201_CREATED - - response2 = auth_client.get('/editions/ed2022/projects') - json = response2.json() - - assert len(json['projects'][0]['projectRoles']) == 4 - assert json['projects'][0]['projectRoles'][3]['skillId'] == 3 - - -def test_add_ghost_student_project(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project 1", edition=edition) + skill: Skill = Skill(name="skill 1") + project_role: ProjectRole = ProjectRole(project=project, skill=skill, slots=1) + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, + edition=edition + ) + database_session.add(project_role) + database_session.add(student) + await database_session.commit() + + await auth_client.coach(edition) + async with auth_client: + resp = await auth_client.post( + f"/editions/{edition.name}/projects/{project.project_id}/roles/{project_role.project_role_id}/students/{student.student_id}", + json={"argumentation": "argumentation"} + ) + assert resp.status_code == status.HTTP_201_CREATED # Only test status code, 'cause we test the post already + resp = await auth_client.post( + f"/editions/{edition.name}/projects/{project.project_id}/roles/{project_role.project_role_id}/students/{student.student_id}", + json={"argumentation": "argumentation_new"} + ) + assert resp.status_code == status.HTTP_409_CONFLICT + + +async def test_add_pr_suggestion_non_existing_student(database_session: AsyncSession, auth_client: AuthClient): """Tests adding a non-existing student to a project""" - auth_client.coach(current_edition) - - student10: list[Student] = database_with_data.query( - Student).where(Student.student_id == 10).all() - assert len(student10) == 0 - response = auth_client.get('/editions/ed2022/projects/1') - json = response.json() - assert len(json['projectRoles']) == 3 - - resp = auth_client.post( - "/editions/ed2022/projects/1/students/10", json={"skill_id": 3}) - assert resp.status_code == status.HTTP_404_NOT_FOUND - - response = auth_client.get('/editions/ed2022/projects/1') - json = response.json() - assert len(json['projectRoles']) == 3 - - -def test_add_student_project_non_existing_skill(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project 1", edition=edition) + skill: Skill = Skill(name="skill 1") + project_role: ProjectRole = ProjectRole(project=project, skill=skill, slots=1) + database_session.add(project_role) + await database_session.commit() + + await auth_client.coach(edition) + async with auth_client: + resp = await auth_client.post( + f"/editions/{edition.name}/projects/{project.project_id}/roles/{project_role.project_role_id}/students/0", + json={"argumentation": "argumentation"} + ) + assert resp.status_code == status.HTTP_404_NOT_FOUND + + response = await auth_client.get(f'/editions/{edition.name}/projects/{project.project_id}') + json = response.json() + assert len(json['projectRoles']) == 1 + assert len(json['projectRoles'][0]['suggestions']) == 0 + + +async def test_add_pr_suggestion_non_existing_pr(database_session: AsyncSession, auth_client: AuthClient): """Tests adding a non-existing student to a project""" - auth_client.coach(current_edition) - - skill10: list[Skill] = database_with_data.query( - Skill).where(Skill.skill_id == 10).all() - assert len(skill10) == 0 - response = auth_client.get('/editions/ed2022/projects/1') - json = response.json() - assert len(json['projectRoles']) == 3 - - resp = auth_client.post( - "/editions/ed2022/projects/1/students/3", json={"skill_id": 10}) - assert resp.status_code == status.HTTP_404_NOT_FOUND - - response = auth_client.get('/editions/ed2022/projects/1') - json = response.json() - assert len(json['projectRoles']) == 3 - - -def test_add_student_to_ghost_project(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): - """Tests adding a student to a project that doesn't exist""" - auth_client.coach(current_edition) - project10: list[Project] = database_with_data.query( - Project).where(Project.project_id == 10).all() - assert len(project10) == 0 - - resp = auth_client.post( - "/editions/ed2022/projects/10/students/1", json={"skill_id": 1}) - assert resp.status_code == status.HTTP_404_NOT_FOUND - - -def test_add_incomplete_data_student_project(database_session: Session, auth_client: AuthClient): - """Tests adding a student with incomplete data""" - - edition = Edition(year=2022, name="ed2022") - database_session.add(edition) - project = Project(name="project", edition_id=1, - project_id=1, number_of_students=2) + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project 1", edition=edition) + skill: Skill = Skill(name="skill 1") + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, + edition=edition + ) database_session.add(project) - database_session.commit() - - auth_client.coach(edition) - resp = auth_client.post( - "/editions/ed2022/projects/1/students/1", json={}) - - assert resp.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - - response2 = auth_client.get('/editions/ed2022/projects') - json = response2.json() - - assert len(json['projects'][0]['projectRoles']) == 0 - - -def test_change_student_project(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): + database_session.add(student) + database_session.add(skill) + await database_session.commit() + + await auth_client.coach(edition) + + async with auth_client: + resp = await auth_client.post( + f"/editions/{edition.name}/projects/{project.project_id}/roles/0/students/{student.student_id}", + json={"argumentation": "argumentation"} + ) + assert resp.status_code == status.HTTP_404_NOT_FOUND + assert len((await database_session.execute(select(ProjectRoleSuggestion))).scalars().all()) == 0 + + +async def test_add_pr_suggestion_readonly_edition(database_session: AsyncSession, auth_client: AuthClient): + """tests add a student to a project from an old edition""" + edition: Edition = Edition(year=2022, name="ed2022", readonly=True) + project: Project = Project(name="project 1", edition=edition) + skill: Skill = Skill(name="skill 1") + project_role: ProjectRole = ProjectRole(project=project, skill=skill, slots=1) + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, + edition=edition + ) + database_session.add(project_role) + database_session.add(student) + database_session.add(Edition(year=2023, name="ed2023")) + await database_session.commit() + + await auth_client.coach(edition) + + await database_session.commit() + + async with auth_client: + resp = await auth_client.post( + f"/editions/{edition.name}/projects/{project.project_id}/roles/{project_role.project_role_id}/students/{student.student_id}", + json={"argumentation": "argumentation"} + ) + assert resp.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + + +async def test_change_pr_suggestion(database_session: AsyncSession, auth_client: AuthClient): """Tests changing a student's project""" - auth_client.coach(current_edition) - - resp1 = auth_client.patch( - "/editions/ed2022/projects/1/students/1", json={"skill_id": 4}) - - assert resp1.status_code == status.HTTP_204_NO_CONTENT - - response2 = auth_client.get('/editions/ed2022/projects') - json = response2.json() - - assert len(json['projects'][0]['projectRoles']) == 3 - assert json['projects'][0]['projectRoles'][0]['skillId'] == 4 - - -def test_change_incomplete_data_student_project(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): - """Tests changing a student's project with incomplete data""" - auth_client.coach(current_edition) - - resp1 = auth_client.patch( - "/editions/ed2022/projects/1/students/1", json={}) - - assert resp1.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - - response2 = auth_client.get('/editions/ed2022/projects') - json = response2.json() - - assert len(json['projects'][0]['projectRoles']) == 3 - assert json['projects'][0]['projectRoles'][0]['skillId'] == 1 - - -def test_change_ghost_student_project(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project 1", edition=edition) + skill: Skill = Skill(name="skill 1") + project_role: ProjectRole = ProjectRole(project=project, skill=skill, slots=1) + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, + edition=edition + ) + pr_suggestion: ProjectRoleSuggestion = ProjectRoleSuggestion(project_role=project_role, student=student) + database_session.add(pr_suggestion) + await database_session.commit() + + await auth_client.coach(edition) + + async with auth_client: + resp = await auth_client.patch( + f"/editions/{edition.name}/projects/{project.project_id}/roles/{project_role.project_role_id}/students/{student.student_id}", + json={"argumentation": "argumentation"} + ) + assert resp.status_code == status.HTTP_204_NO_CONTENT + + response2 = await auth_client.get(f'/editions/{edition.name}/projects/{project.project_id}') + json = response2.json() + assert len(json['projectRoles']) == 1 + assert len(json['projectRoles'][0]['suggestions']) == 1 + assert json['projectRoles'][0]['suggestions'][0]['argumentation'] == 'argumentation' + + +async def test_change_pr_suggestion_non_existing_student(database_session: AsyncSession, auth_client: AuthClient): """Tests changing a non-existing student of a project""" - auth_client.coach(current_edition) - - student10: list[Student] = database_with_data.query( - Student).where(Student.student_id == 10).all() - assert len(student10) == 0 - response = auth_client.get('/editions/ed2022/projects/1') - json = response.json() - assert len(json['projectRoles']) == 3 - - resp = auth_client.patch( - "/editions/ed2022/projects/1/students/10", json={"skill_id": 4}) - assert resp.status_code == status.HTTP_404_NOT_FOUND - - response = auth_client.get('/editions/ed2022/projects/1') - json = response.json() - assert len(json['projectRoles']) == 3 - - -def test_change_student_project_non_existing_skill(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): - """Tests deleting a student from a project that isn't assigned""" - auth_client.coach(current_edition) - - skill10: list[Skill] = database_with_data.query( - Skill).where(Skill.skill_id == 10).all() - assert len(skill10) == 0 - - response = auth_client.get('/editions/ed2022/projects/1') - json = response.json() - assert len(json['projectRoles']) == 3 - - resp = auth_client.patch( - "/editions/ed2022/projects/1/students/3", json={"skill_id": 10}) - assert resp.status_code == status.HTTP_404_NOT_FOUND - - response = auth_client.get('/editions/ed2022/projects/1') - json = response.json() - assert len(json['projectRoles']) == 3 - - -def test_change_student_project_ghost_drafter(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): - """Tests changing a drafter of a ProjectRole to a non-existing one""" - auth_client.coach(current_edition) - user10: list[User] = database_with_data.query( - User).where(User.user_id == 10).all() - assert len(user10) == 0 - - response = auth_client.get('/editions/ed2022/projects/1') - json = response.json() - assert len(json['projectRoles']) == 3 - - resp = auth_client.patch( - "/editions/ed2022/projects/1/students/3", json={"skill_id": 4}) - assert resp.status_code == status.HTTP_404_NOT_FOUND - - response = auth_client.get('/editions/ed2022/projects/1') - json = response.json() - assert len(json['projectRoles']) == 3 - - -def test_change_student_to_ghost_project(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): - """Tests changing a student of a project that doesn't exist""" - auth_client.coach(current_edition) - project10: list[Project] = database_with_data.query( - Project).where(Project.project_id == 10).all() - assert len(project10) == 0 - - resp = auth_client.patch( - "/editions/ed2022/projects/10/students/1", json={"skill_id": 1}) - assert resp.status_code == status.HTTP_404_NOT_FOUND - - -def test_delete_student_project(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): - """Tests deleting a student from a project""" - auth_client.coach(current_edition) - resp = auth_client.delete("/editions/ed2022/projects/1/students/1") - - assert resp.status_code == status.HTTP_204_NO_CONTENT + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project 1", edition=edition) + skill: Skill = Skill(name="skill 1") + project_role: ProjectRole = ProjectRole(project=project, skill=skill, slots=1) + database_session.add(project_role) + await database_session.commit() - response2 = auth_client.get('/editions/ed2022/projects') - json = response2.json() + await auth_client.coach(edition) - assert len(json['projects'][0]['projectRoles']) == 2 + async with auth_client: + resp = await auth_client.patch( + f"/editions/{edition.name}/projects/{project.project_id}/roles/{project_role.project_role_id}/students/0", + json={"argumentation": "argumentation"} + ) + assert resp.status_code == status.HTTP_404_NOT_FOUND -def test_delete_student_project_empty(database_session: Session, auth_client: AuthClient): +async def test_change_pr_suggestion_non_existing_pr(database_session: AsyncSession, auth_client: AuthClient): """Tests deleting a student from a project that isn't assigned""" - - edition = Edition(year=2022, name="ed2022") - database_session.add(edition) - project = Project(name="project", edition_id=1, - project_id=1, number_of_students=2) + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project 1", edition=edition) + skill: Skill = Skill(name="skill 1") + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, + edition=edition + ) database_session.add(project) - database_session.commit() + database_session.add(student) + database_session.add(skill) + await database_session.commit() - auth_client.coach(edition) - resp = auth_client.delete("/editions/ed2022/projects/1/students/1") + await auth_client.coach(edition) - assert resp.status_code == status.HTTP_404_NOT_FOUND + async with auth_client: + resp = await auth_client.patch( + f"/editions/{edition.name}/projects/{project.project_id}/roles/0/students/{student.student_id}", + json={"argumentation": "argumentation"} + ) + assert resp.status_code == status.HTTP_404_NOT_FOUND -def test_get_conflicts(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): +async def test_delete_pr_suggestion(database_session: AsyncSession, auth_client: AuthClient): + """Tests deleting a student from a project""" + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project 1", edition=edition) + skill: Skill = Skill(name="skill 1") + project_role: ProjectRole = ProjectRole(project=project, skill=skill, slots=1) + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, + edition=edition + ) + pr_suggestion: ProjectRoleSuggestion = ProjectRoleSuggestion(project_role=project_role, student=student) + database_session.add(pr_suggestion) + await database_session.commit() + + await auth_client.coach(edition) + + async with auth_client: + resp = await auth_client.delete( + f"/editions/{edition.name}/projects/{project.project_id}/roles/{project_role.project_role_id}/students/{student.student_id}" + ) + assert resp.status_code == status.HTTP_204_NO_CONTENT + + response2 = await auth_client.get(f'/editions/{edition.name}/projects/{project.project_id}') + json = response2.json() + assert len(json['projectRoles']) == 1 + assert len(json['projectRoles'][0]['suggestions']) == 0 + + +async def test_delete_pr_suggestion_multiple(database_session: AsyncSession, auth_client: AuthClient): + """Tests deleting a student from a project, with a student being assigned to multiple project_roles""" + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project 1", edition=edition) + project2: Project = Project(name="project 2", edition=edition) + skill: Skill = Skill(name="skill 1") + project_role: ProjectRole = ProjectRole(project=project, skill=skill, slots=1) + project_role2: ProjectRole = ProjectRole(project=project2, skill=skill, slots=1) + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, + edition=edition + ) + pr_suggestion: ProjectRoleSuggestion = ProjectRoleSuggestion(project_role=project_role, student=student) + pr_suggestion2: ProjectRoleSuggestion = ProjectRoleSuggestion(project_role=project_role2, student=student) + database_session.add(pr_suggestion) + database_session.add(pr_suggestion2) + await database_session.commit() + + await auth_client.coach(edition) + async with auth_client: + resp = await auth_client.delete( + f"/editions/{edition.name}/projects/{project.project_id}/roles/{project_role.project_role_id}/students/{student.student_id}" + ) + assert resp.status_code == status.HTTP_204_NO_CONTENT + + response2 = await auth_client.get(f'/editions/{edition.name}/projects/{project.project_id}') + json = response2.json() + assert len(json['projectRoles']) == 1 + assert len(json['projectRoles'][0]['suggestions']) == 0 + + +async def test_delete_pr_suggestion_non_existing_pr_suggestion(database_session: AsyncSession, auth_client: AuthClient): + """Tests deleting a pr_suggestion that doesn't exist""" + edition: Edition = Edition(year=2022, name="ed2022") + project: Project = Project(name="project 1", edition=edition) + skill: Skill = Skill(name="skill 1") + project_role: ProjectRole = ProjectRole(project=project, skill=skill, slots=1) + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, + edition=edition + ) + database_session.add(project_role) + database_session.add(student) + await database_session.commit() + + await auth_client.coach(edition) + + async with auth_client: + resp = await auth_client.delete( + f"/editions/{edition.name}/projects/{project.project_id}/roles/{project_role.project_role_id}/students/{student.student_id}" + ) + assert resp.status_code == status.HTTP_404_NOT_FOUND + + +async def test_get_conflicts(database_session: AsyncSession, auth_client: AuthClient): """Test getting the conflicts""" - auth_client.coach(current_edition) - response = auth_client.get("/editions/ed2022/projects/conflicts") - json = response.json() - assert len(json['conflictStudents']) == 1 - assert json['conflictStudents'][0]['student']['studentId'] == 1 - assert len(json['conflictStudents'][0]['projects']) == 2 - - -def test_add_student_project_old_edition(database_with_data: Session, auth_client: AuthClient): - """tests add a student to a project from an old edition""" - auth_client.admin() - database_with_data.add(Edition(year=2023, name="ed2023")) - database_with_data.commit() - - resp = auth_client.post( - "/editions/ed2022/projects/1/students/3", json={"skill_id": 1, "drafter_id": 1}) - - assert resp.status_code == status.HTTP_405_METHOD_NOT_ALLOWED - - -def test_add_student_same_project_role(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): - """Two different students can't have the same project_role""" - auth_client.coach(current_edition) - - resp = auth_client.post( - "/editions/ed2022/projects/1/students/3", json={"skill_id": 2}) - - assert resp.status_code == status.HTTP_400_BAD_REQUEST - - -def test_add_student_project_wrong_project_skill(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): - """A project_role can't be created if the project doesn't require the skill""" - auth_client.coach(current_edition) - - resp = auth_client.post( - "/editions/ed2022/projects/3/students/3", json={"skill_id": 4}) - - assert resp.status_code == status.HTTP_400_BAD_REQUEST - - -def test_add_student_project_wrong_student_skill(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): - """A project_role can't be created if the student doesn't have the skill""" - auth_client.coach(current_edition) - - resp = auth_client.post( - "/editions/ed2022/projects/1/students/2", json={"skill_id": 1}) - - assert resp.status_code == status.HTTP_400_BAD_REQUEST - - -def test_add_student_project_already_confirmed(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): - """A project_role can't be cre created if the student involved has already been confirmed elsewhere""" - auth_client.coach(current_edition) - - resp = auth_client.post("/editions/ed2022/projects/1/students/4", json={"skill_id": 3}) - - assert resp.status_code == status.HTTP_400_BAD_REQUEST - - -def test_confirm_project_role(database_with_data: Session, auth_client: AuthClient): - """Confirm a project role for a student without conflicts""" - auth_client.admin() - resp = auth_client.post( - "/editions/ed2022/projects/1/students/3", json={"skill_id": 3}) - - assert resp.status_code == status.HTTP_201_CREATED - - response2 = auth_client.post( - "/editions/ed2022/projects/1/students/3/confirm") - - assert response2.status_code == status.HTTP_204_NO_CONTENT - pr = database_with_data.query(ProjectRole).where(ProjectRole.student_id == 3) \ - .where(ProjectRole.project_id == 1).one() - assert pr.definitive is True - - -def test_confirm_project_role_conflict(database_with_data: Session, auth_client: AuthClient): - """A student who is part of a conflict can't have their project_role confirmed""" - auth_client.admin() - response2 = auth_client.post( - "/editions/ed2022/projects/1/students/1/confirm") - - assert response2.status_code == status.HTTP_409_CONFLICT + edition: Edition = Edition(year=2022, name="ed2022") + skill: Skill = Skill(name="skill 1") + student: Student = Student( + first_name="Jos", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@mail.com", + phone_number="0487/86.24.45", + alumni=True, + wants_to_be_student_coach=True, + edition=edition + ) + + database_session.add(Student( + first_name="Jos2", + last_name="Vermeulen", + preferred_name="Joske", + email_address="josvermeulen@gmail.com", + phone_number="0487/86.24.46", + alumni=True, + wants_to_be_student_coach=True, + edition=edition + )) + + database_session.add(ProjectRoleSuggestion( + student=student, + project_role=ProjectRole( + skill=skill, + slots=1, + project=Project( + name="project 1", + edition=edition + ) + ) + )) + + database_session.add(ProjectRoleSuggestion( + student=student, + project_role=ProjectRole( + skill=skill, + slots=1, + project=Project( + name="project 2", + edition=edition + ) + ) + )) + await database_session.commit() + + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get(f"/editions/{edition.name}/projects/conflicts") + json = response.json() + assert len(json['conflictStudents']) == 1 + assert json['conflictStudents'][0]['studentId'] == student.student_id + assert len(json['conflictStudents'][0]['prSuggestions']) == 2 diff --git a/backend/tests/test_routers/test_editions/test_register/test_register.py b/backend/tests/test_routers/test_editions/test_register/test_register.py index 6d75d523c..af0546bb3 100644 --- a/backend/tests/test_routers/test_editions/test_register/test_register.py +++ b/backend/tests/test_routers/test_editions/test_register/test_register.py @@ -1,77 +1,86 @@ +from unittest.mock import AsyncMock + from sqlalchemy.orm import Session +from httpx import AsyncClient +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status -from starlette.testclient import TestClient from src.database.models import Edition, InviteLink, User, AuthEmail -def test_ok(database_session: Session, test_client: TestClient): +async def test_ok(database_session: AsyncSession, test_client: AsyncClient): """Tests a registeration is made""" edition: Edition = Edition(year=2022, name="ed2022") invite_link: InviteLink = InviteLink( edition=edition, target_email="jw@gmail.com") database_session.add(edition) database_session.add(invite_link) - database_session.commit() - response = test_client.post("/editions/ed2022/register/email", json={ - "name": "Joskes vermeulen", "email": "jw@gmail.com", "pw": "test", - "uuid": str(invite_link.uuid)}) - assert response.status_code == status.HTTP_201_CREATED - user: User = database_session.query(User).where( - User.name == "Joskes vermeulen").one() - user_auth: AuthEmail = database_session.query(AuthEmail).where(AuthEmail.email == "jw@gmail.com").one() - assert user.user_id == user_auth.user_id - - -def test_use_uuid_multiple_times(database_session: Session, test_client: TestClient): + await database_session.commit() + async with test_client: + response = await test_client.post("/editions/ed2022/register/email", json={ + "name": "Joskes vermeulen", "email": "jw@gmail.com", "pw": "test", + "uuid": str(invite_link.uuid)}) + assert response.status_code == status.HTTP_201_CREATED + user: User = (await database_session.execute(select(User).where( + User.name == "Joskes vermeulen"))).unique().scalar_one() + user_auth: AuthEmail = (await database_session.execute(select(AuthEmail).where(AuthEmail.email == "jw@gmail.com"))).scalar_one() + assert user.user_id == user_auth.user_id + + +async def test_use_uuid_multiple_times(database_session: AsyncSession, test_client: AsyncClient): """Tests that you can't use the same UUID multiple times""" edition: Edition = Edition(year=2022, name="ed2022") invite_link: InviteLink = InviteLink( edition=edition, target_email="jw@gmail.com") database_session.add(edition) database_session.add(invite_link) - database_session.commit() - test_client.post("/editions/ed2022/register/email", json={ - "name": "Joskes vermeulen", "email": "jw@gmail.com", "pw": "test", - "uuid": str(invite_link.uuid)}) - response = test_client.post("/editions/ed2022/register/email", json={ - "name": "Joske Vermeulen", "email": "jw2@gmail.com", "pw": "test", - "uuid": str(invite_link.uuid)}) - assert response.status_code == status.HTTP_404_NOT_FOUND - - -def test_no_valid_uuid(database_session: Session, test_client: TestClient): + await database_session.commit() + async with test_client: + await test_client.post("/editions/ed2022/register/email", json={ + "name": "Joskes vermeulen", "email": "jw@gmail.com", "pw": "test", + "uuid": str(invite_link.uuid)}) + response = await test_client.post("/editions/ed2022/register/email", json={ + "name": "Joske Vermeulen", "email": "jw2@gmail.com", "pw": "test", + "uuid": str(invite_link.uuid)}) + assert response.status_code == status.HTTP_404_NOT_FOUND + + +async def test_no_valid_uuid(database_session: AsyncSession, test_client: AsyncClient): """Tests that no valid uuid, can't make a account""" edition: Edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() - response = test_client.post("/editions/ed2022/register/email", json={ - "name": "Joskes vermeulen", "email": "jw@gmail.com", "pw": "test", - "uuid": "550e8400-e29b-41d4-a716-446655440000"}) - assert response.status_code == status.HTTP_404_NOT_FOUND - users: list[User] = database_session.query( - User).where(User.name == "Joskes vermeulen").all() - assert len(users) == 0 - - -def test_no_edition(database_session: Session, test_client: TestClient): + await database_session.commit() + async with test_client: + response = await test_client.post("/editions/ed2022/register/email", json={ + "name": "Joskes vermeulen", "email": "jw@gmail.com", "pw": "test", + "uuid": "550e8400-e29b-41d4-a716-446655440000"}) + assert response.status_code == status.HTTP_404_NOT_FOUND + users: list[User] = (await database_session.execute(select( + User).where(User.name == "Joskes vermeulen"))).scalars().all() + assert len(users) == 0 + + +async def test_no_edition(database_session: AsyncSession, test_client: AsyncClient): """Tests if there is no edition it gets the right error code""" - response = test_client.post("/editions/ed2022/register/email", json={ - "name": "Joskes vermeulen", "email": "jw@gmail.com", "pw": "test"}) + async with test_client: + response = await test_client.post("/editions/ed2022/register/email", json={ + "name": "Joskes vermeulen", "email": "jw@gmail.com", "pw": "test"}) assert response.status_code == status.HTTP_404_NOT_FOUND -def test_not_a_correct_email(database_session: Session, test_client: TestClient): +async def test_not_a_correct_email(database_session: AsyncSession, test_client: AsyncClient): """Tests when the email isn't correct, it gets the right error code""" database_session.add(Edition(year=2022, name="ed2022")) - database_session.commit() - response = test_client.post("/editions/ed2022/register/email", - json={"name": "Joskes vermeulen", "email": "jw", "pw": "test"}) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + await database_session.commit() + async with test_client: + response = await test_client.post("/editions/ed2022/register/email", + json={"name": "Joskes vermeulen", "email": "jw", "pw": "test"}) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY -def test_duplicate_user(database_session: Session, test_client: TestClient): +async def test_duplicate_user(database_session: AsyncSession, test_client: AsyncClient): """Tests when there is a duplicate, it gets the right error code""" edition: Edition = Edition(year=2022, name="ed2022") invite_link1: InviteLink = InviteLink( @@ -81,27 +90,68 @@ def test_duplicate_user(database_session: Session, test_client: TestClient): database_session.add(edition) database_session.add(invite_link1) database_session.add(invite_link2) - database_session.commit() - test_client.post("/editions/ed2022/register/email", - json={"name": "Joskes vermeulen", "email": "jw@gmail.com", "pw": "test", - "uuid": str(invite_link1.uuid)}) - response = test_client.post("/editions/ed2022/register/email", json={ - "name": "Joske vermeulen", "email": "jw@gmail.com", "pw": "test1", - "uuid": str(invite_link2.uuid)}) - assert response.status_code == status.HTTP_400_BAD_REQUEST - - -def test_old_edition(database_session: Session, test_client: TestClient): + await database_session.commit() + async with test_client: + await test_client.post("/editions/ed2022/register/email", + json={"name": "Joskes vermeulen", "email": "jw@gmail.com", "pw": "test", + "uuid": str(invite_link1.uuid)}) + response = await test_client.post("/editions/ed2022/register/email", json={ + "name": "Joske vermeulen", "email": "jw@gmail.com", "pw": "test1", + "uuid": str(invite_link2.uuid)}) + assert response.status_code == status.HTTP_409_CONFLICT + + +async def test_readonly_edition(database_session: AsyncSession, test_client: AsyncClient): """Tests trying to make a registration for a read-only edition""" - edition: Edition = Edition(year=2022, name="ed2022") + edition: Edition = Edition(year=2022, name="ed2022", readonly=True) edition3: Edition = Edition(year=2023, name="ed2023") invite_link: InviteLink = InviteLink( edition=edition, target_email="jw@gmail.com") database_session.add(edition) database_session.add(edition3) database_session.add(invite_link) - database_session.commit() - response = test_client.post("/editions/ed2022/register/email", json={ - "name": "Joskes vermeulen", "email": "jw@gmail.com", "pw": "test", - "uuid": str(invite_link.uuid)}) + await database_session.commit() + async with test_client: + response = await test_client.post("/editions/ed2022/register/email", json={ + "name": "Joskes vermeulen", "email": "jw@gmail.com", "pw": "test", + "uuid": str(invite_link.uuid)}) assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + + +async def test_github_register(database_session: AsyncSession, test_client: AsyncClient, aiohttp_session: AsyncMock): + """Test registering with github""" + edition: Edition = Edition(year=2022, name="ed2022") + invite_link: InviteLink = InviteLink( + edition=edition, target_email="jw@gmail.com") + database_session.add(edition) + database_session.add(invite_link) + await database_session.commit() + + # Request that gets an access token + first_response = AsyncMock() + first_response.status = 200 + first_response.json.return_value = { + "access_token": "token", + "scope": "read:user,user:email" + } + + # Request that gets the user's id + second_response = AsyncMock() + second_response.status = 200 + second_response.json.return_value = { + "name": "name", + "email": "email", + "id": 1 + } + + aiohttp_session.post.return_value = first_response + aiohttp_session.get.return_value = second_response + + async with test_client: + resp = await test_client.post("/editions/ed2022/register/github", json={"code": "code", "uuid": str(invite_link.uuid)}) + + assert resp.status_code == status.HTTP_201_CREATED + + users = (await database_session.execute(select(User))).unique().scalars().all() + assert len(users) == 1 + assert users[0].name == "name" diff --git a/backend/tests/test_routers/test_editions/test_students/test_answers/__init__.py b/backend/tests/test_routers/test_editions/test_students/test_answers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/tests/test_routers/test_editions/test_students/test_answers/test_answers.py b/backend/tests/test_routers/test_editions/test_students/test_answers/test_answers.py new file mode 100644 index 000000000..b63e9c38b --- /dev/null +++ b/backend/tests/test_routers/test_editions/test_students/test_answers/test_answers.py @@ -0,0 +1,110 @@ +import pytest +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from starlette import status + +from src.database.models import Edition, QuestionFileAnswer, User, Skill, Student, Question, QuestionAnswer +from src.database.enums import QuestionEnum +from tests.utils.authorization import AuthClient + + +@pytest.fixture +async def database_with_data(database_session: AsyncSession) -> AsyncSession: + """fixture for adding data to the database""" + edition: Edition = Edition(year=2022, name="ed2022") + database_session.add(edition) + user: User = User(name="coach1") + database_session.add(user) + skill1: Skill = Skill(name="skill1") + skill2: Skill = Skill(name="skill2") + skill3: Skill = Skill(name="skill3") + database_session.add(skill1) + database_session.add(skill2) + database_session.add(skill3) + student01: Student = Student(first_name="Jos", last_name="Vermeulen", preferred_name="Joske", + email_address="josvermeulen@mail.com", phone_number="0487/86.24.45", alumni=True, + wants_to_be_student_coach=True, edition=edition, skills=[skill1, skill3]) + student02: Student = Student(first_name="Isabella", last_name="Christensen", preferred_name="Isabella", + email_address="isabella.christensen@example.com", phone_number="98389723", alumni=True, + wants_to_be_student_coach=True, edition=edition, skills=[skill2]) + database_session.add(student01) + database_session.add(student02) + question1: Question = Question( + type=QuestionEnum.INPUT_TEXT, question="Tell me something", student=student01, answers=[], files=[]) + question2: Question = Question( + type=QuestionEnum.MULTIPLE_CHOICE, question="Favorite drink", student=student01, answers=[], files=[]) + database_session.add(question1) + database_session.add(question2) + question_answer1: QuestionAnswer = QuestionAnswer( + answer="I like pizza", question=question1) + question_answer2: QuestionAnswer = QuestionAnswer( + answer="ICE TEA", question=question2) + question_answer3: QuestionAnswer = QuestionAnswer( + answer="Cola", question=question2) + database_session.add(question_answer1) + database_session.add(question_answer2) + database_session.add(question_answer3) + question_file_answer: QuestionFileAnswer = QuestionFileAnswer( + file_name="pizza.txt", url="een/link/naar/pizza.txt", mime_type="text/plain", size=16, question=question1 + ) + database_session.add(question_file_answer) + await database_session.commit() + return database_session + + +@pytest.fixture +async def current_edition(database_with_data: AsyncSession) -> Edition: + """fixture to get the latest edition""" + return (await database_with_data.execute(select(Edition))).scalars().all()[-1] + + +async def test_get_answers_not_logged_in(database_with_data: AsyncSession, auth_client: AuthClient): + """test get answers when not logged in""" + async with auth_client: + response = await auth_client.get("/editions/ed2022/students/1/answers", follow_redirects=True) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + +async def test_get_answers_as_coach(database_with_data: AsyncSession, auth_client: AuthClient, + current_edition: Edition): + """test get answers when logged in as coach""" + await auth_client.coach(current_edition) + async with auth_client: + response = await auth_client.get("/editions/ed2022/students/1/answers", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + json = response.json() + assert len(json["qAndA"]) == 2 + assert json["qAndA"][0]["question"] == "Tell me something" + assert len(json["qAndA"][0]["answers"]) == 1 + assert json["qAndA"][0]["answers"][0] == "I like pizza" + assert len(json["qAndA"][0]["files"]) == 1 + assert json["qAndA"][0]["files"][0]["filename"] == "pizza.txt" + assert json["qAndA"][0]["files"][0]["mimeType"] == "text/plain" + assert json["qAndA"][0]["files"][0]["url"] == "een/link/naar/pizza.txt" + assert json["qAndA"][1]["question"] == "Favorite drink" + assert len(json["qAndA"][1]["answers"]) == 2 + assert json["qAndA"][1]["answers"][0] == "ICE TEA" + assert json["qAndA"][1]["answers"][1] == "Cola" + assert len(json["qAndA"][1]["files"]) == 0 + + +async def test_get_answers_as_admin(database_with_data: AsyncSession, auth_client: AuthClient): + """test get answers when logged in as coach""" + await auth_client.admin() + async with auth_client: + response = await auth_client.get("/editions/ed2022/students/1/answers", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + json = response.json() + assert len(json["qAndA"]) == 2 + assert json["qAndA"][0]["question"] == "Tell me something" + assert len(json["qAndA"][0]["answers"]) == 1 + assert json["qAndA"][0]["answers"][0] == "I like pizza" + assert len(json["qAndA"][0]["files"]) == 1 + assert json["qAndA"][0]["files"][0]["filename"] == "pizza.txt" + assert json["qAndA"][0]["files"][0]["mimeType"] == "text/plain" + assert json["qAndA"][0]["files"][0]["url"] == "een/link/naar/pizza.txt" + assert json["qAndA"][1]["question"] == "Favorite drink" + assert len(json["qAndA"][1]["answers"]) == 2 + assert json["qAndA"][1]["answers"][0] == "ICE TEA" + assert json["qAndA"][1]["answers"][1] == "Cola" + assert len(json["qAndA"][1]["files"]) == 0 diff --git a/backend/tests/test_routers/test_editions/test_students/test_students.py b/backend/tests/test_routers/test_editions/test_students/test_students.py index 59ee28291..33ef33d5c 100644 --- a/backend/tests/test_routers/test_editions/test_students/test_students.py +++ b/backend/tests/test_routers/test_editions/test_students/test_students.py @@ -1,7 +1,8 @@ -import datetime import pytest -from sqlalchemy.orm import Session +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status + from settings import DB_PAGE_SIZE from src.database.enums import DecisionEnum, EmailStatusEnum from src.database.models import Student, Edition, Skill, DecisionEmail @@ -10,28 +11,26 @@ @pytest.fixture -def database_with_data(database_session: Session) -> Session: +async def database_with_data(database_session: AsyncSession) -> AsyncSession: """A fixture to fill the database with fake data that can easly be used when testing""" # Editions edition: Edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() # Skill - skill1: Skill = Skill(name="skill1", description="something about skill1") - skill2: Skill = Skill(name="skill2", description="something about skill2") - skill3: Skill = Skill(name="skill3", description="something about skill3") - skill4: Skill = Skill(name="skill4", description="something about skill4") - skill5: Skill = Skill(name="skill5", description="something about skill5") - skill6: Skill = Skill(name="skill6", description="something about skill6") + skill1: Skill = Skill(name="skill1") + skill2: Skill = Skill(name="skill2") + skill3: Skill = Skill(name="skill3") + skill4: Skill = Skill(name="skill4") + skill5: Skill = Skill(name="skill5") + skill6: Skill = Skill(name="skill6") database_session.add(skill1) database_session.add(skill2) database_session.add(skill3) database_session.add(skill4) database_session.add(skill5) database_session.add(skill6) - database_session.commit() # Student student01: Student = Student(first_name="Jos", last_name="Vermeulen", preferred_name="Joske", @@ -39,500 +38,621 @@ def database_with_data(database_session: Session) -> Session: wants_to_be_student_coach=True, edition=edition, skills=[skill1, skill3, skill6]) student30: Student = Student(first_name="Marta", last_name="Marquez", preferred_name="Marta", email_address="marta.marquez@example.com", phone_number="967-895-285", alumni=False, - wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + decision=DecisionEnum.YES, wants_to_be_student_coach=False, edition=edition, + skills=[skill2, skill4, skill5]) database_session.add(student01) database_session.add(student30) - database_session.commit() + await database_session.commit() return database_session -def test_set_definitive_decision_no_authorization(database_with_data: Session, auth_client: AuthClient): +@pytest.fixture +async def current_edition(database_with_data: AsyncSession) -> Edition: + """fixture to get the latest edition""" + return (await database_with_data.execute(select(Edition))).scalars().all()[-1] + + +async def test_set_definitive_decision_no_authorization(database_with_data: AsyncSession, auth_client: AuthClient): """tests that you have to be logged in""" - assert auth_client.put( - "/editions/ed2022/students/2/decision").status_code == status.HTTP_401_UNAUTHORIZED + async with auth_client: + response = await auth_client.put("/editions/ed2022/students/2/decision") + assert response.status_code == status.HTTP_401_UNAUTHORIZED -def test_set_definitive_decision_coach(database_with_data: Session, auth_client: AuthClient): +async def test_set_definitive_decision_coach(database_with_data: AsyncSession, + auth_client: AuthClient, current_edition: Edition): """tests that a coach can't set a definitive decision""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - assert auth_client.put( - "/editions/ed2022/students/2/decision").status_code == status.HTTP_403_FORBIDDEN + await auth_client.coach(current_edition) + async with auth_client: + response = await auth_client.put("/editions/ed2022/students/2/decision") + assert response.status_code == status.HTTP_403_FORBIDDEN -def test_set_definitive_decision_on_ghost(database_with_data: Session, auth_client: AuthClient): +async def test_set_definitive_decision_on_ghost(database_with_data: AsyncSession, auth_client: AuthClient): """tests that you get a 404 if a student don't exicist""" - auth_client.admin() - assert auth_client.put( - "/editions/ed2022/students/100/decision").status_code == status.HTTP_404_NOT_FOUND + await auth_client.admin() + async with auth_client: + response = await auth_client.put("/editions/ed2022/students/100/decision") + assert response.status_code == status.HTTP_404_NOT_FOUND -def test_set_definitive_decision_wrong_body(database_with_data: Session, auth_client: AuthClient): +async def test_set_definitive_decision_wrong_body(database_with_data: AsyncSession, auth_client: AuthClient): """tests you got a 422 if you give a wrong body""" - auth_client.admin() - assert auth_client.put( - "/editions/ed2022/students/1/decision").status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + await auth_client.admin() + async with auth_client: + response = await auth_client.put("/editions/ed2022/students/1/decision") + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY -def test_set_definitive_decision_yes(database_with_data: Session, auth_client: AuthClient): +async def test_set_definitive_decision_yes(database_with_data: AsyncSession, auth_client: AuthClient): """tests that an admin can set a yes""" - auth_client.admin() - assert auth_client.put("/editions/ed2022/students/1/decision", - json={"decision": 1}).status_code == status.HTTP_204_NO_CONTENT - student: Student = database_with_data.query( - Student).where(Student.student_id == 1).one() - assert student.decision == DecisionEnum.YES + await auth_client.admin() + async with auth_client: + response = await auth_client.put("/editions/ed2022/students/1/decision", json={"decision": 1}) + assert response.status_code == status.HTTP_204_NO_CONTENT + query = select(Student).where(Student.student_id == 1) + result = await database_with_data.execute(query) + student: Student = result.unique().scalars().one() + assert student.decision == DecisionEnum.YES -def test_set_definitive_decision_no(database_with_data: Session, auth_client: AuthClient): +async def test_set_definitive_decision_no(database_with_data: AsyncSession, auth_client: AuthClient): """tests that an admin can set a no""" - auth_client.admin() - assert auth_client.put("/editions/ed2022/students/1/decision", - json={"decision": 3}).status_code == status.HTTP_204_NO_CONTENT - student: Student = database_with_data.query( - Student).where(Student.student_id == 1).one() - assert student.decision == DecisionEnum.NO + await auth_client.admin() + async with auth_client: + response = await auth_client.put("/editions/ed2022/students/1/decision", json={"decision": 3}) + assert response.status_code == status.HTTP_204_NO_CONTENT + query = select(Student).where(Student.student_id == 1) + result = await database_with_data.execute(query) + student: Student = result.unique().scalars().one() + assert student.decision == DecisionEnum.NO -def test_set_definitive_decision_maybe(database_with_data: Session, auth_client: AuthClient): +async def test_set_definitive_decision_maybe(database_with_data: AsyncSession, auth_client: AuthClient): """tests that an admin can set a maybe""" - auth_client.admin() - assert auth_client.put("/editions/ed2022/students/1/decision", - json={"decision": 2}).status_code == status.HTTP_204_NO_CONTENT - student: Student = database_with_data.query( - Student).where(Student.student_id == 1).one() - assert student.decision == DecisionEnum.MAYBE + await auth_client.admin() + async with auth_client: + response = await auth_client.put("/editions/ed2022/students/1/decision", json={"decision": 2}) + assert response.status_code == status.HTTP_204_NO_CONTENT + query = select(Student).where(Student.student_id == 1) + result = await database_with_data.execute(query) + student: Student = result.unique().scalars().one() + assert student.decision == DecisionEnum.MAYBE -def test_delete_student_no_authorization(database_with_data: Session, auth_client: AuthClient): +async def test_delete_student_no_authorization(database_with_data: AsyncSession, auth_client: AuthClient): """tests that you have to be logged in""" - assert auth_client.delete( - "/editions/ed2022/students/2").status_code == status.HTTP_401_UNAUTHORIZED + async with auth_client: + response = await auth_client.delete("/editions/ed2022/students/2") + assert response.status_code == status.HTTP_401_UNAUTHORIZED -def test_delete_student_coach(database_with_data: Session, auth_client: AuthClient): +async def test_delete_student_coach(database_with_data: AsyncSession, auth_client: AuthClient, + current_edition: Edition): """tests that a coach can't delete a student""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - assert auth_client.delete( - "/editions/ed2022/students/2").status_code == status.HTTP_403_FORBIDDEN - students: Student = database_with_data.query( - Student).where(Student.student_id == 1).all() - assert len(students) == 1 + await auth_client.coach(current_edition) + async with auth_client: + response = await auth_client.delete("/editions/ed2022/students/2") + assert response.status_code == status.HTTP_403_FORBIDDEN + query = select(Student).where(Student.student_id == 1) + result = await database_with_data.execute(query) + students: list[Student] = result.unique().scalars().all() + assert len(students) == 1 -def test_delete_ghost(database_with_data: Session, auth_client: AuthClient): +async def test_delete_ghost(database_with_data: AsyncSession, auth_client: AuthClient): """tests that you can't delete a student that don't excist""" - auth_client.admin() - assert auth_client.delete( - "/editions/ed2022/students/100").status_code == status.HTTP_404_NOT_FOUND - students: Student = database_with_data.query( - Student).where(Student.student_id == 1).all() - assert len(students) == 1 + await auth_client.admin() + async with auth_client: + response = await auth_client.delete("/editions/ed2022/students/100") + assert response.status_code == status.HTTP_404_NOT_FOUND + query = select(Student).where(Student.student_id == 1) + result = await database_with_data.execute(query) + students: list[Student] = result.unique().scalars().all() + assert len(students) == 1 -def test_delete(database_with_data: Session, auth_client: AuthClient): +async def test_delete(database_with_data: AsyncSession, auth_client: AuthClient): """tests an admin can delete a student""" - auth_client.admin() - assert auth_client.delete( - "/editions/ed2022/students/1").status_code == status.HTTP_204_NO_CONTENT - students: Student = database_with_data.query( - Student).where(Student.student_id == 1).all() - assert len(students) == 0 + await auth_client.admin() + async with auth_client: + response = await auth_client.delete("/editions/ed2022/students/1") + assert response.status_code == status.HTTP_204_NO_CONTENT + query = select(Student).where(Student.student_id == 1) + result = await database_with_data.execute(query) + students: list[Student] = result.unique().scalars().all() + assert len(students) == 0 -def test_get_student_by_id_no_autorization(database_with_data: Session, auth_client: AuthClient): +async def test_get_student_by_id_no_autorization(database_with_data: AsyncSession, auth_client: AuthClient): """tests you have to be logged in to get a student by id""" - assert auth_client.get( - "/editions/ed2022/students/1").status_code == status.HTTP_401_UNAUTHORIZED + async with auth_client: + response = await auth_client.get("/editions/ed2022/students/1") + assert response.status_code == status.HTTP_401_UNAUTHORIZED -def test_get_student_by_id(database_with_data: Session, auth_client: AuthClient): +async def test_get_student_by_id(database_with_data: AsyncSession, auth_client: AuthClient, + current_edition: Edition): """tests you can get a student by id""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - assert auth_client.get( - "/editions/ed2022/students/1").status_code == status.HTTP_200_OK + await auth_client.coach(current_edition) + async with auth_client: + response = await auth_client.get("/editions/ed2022/students/1") + assert response.status_code == status.HTTP_200_OK -def test_get_student_by_id_wrong_edition(database_with_data: Session, auth_client: AuthClient): +async def test_get_student_by_id_wrong_edition(database_with_data: AsyncSession, auth_client: AuthClient, + current_edition: Edition): """tests you can get a student by id""" edition: Edition = Edition(year=2023, name="ed2023") database_with_data.add(edition) - database_with_data.commit() - auth_client.coach(edition) - assert auth_client.get( - "/editions/ed2023/students/1").status_code == status.HTTP_404_NOT_FOUND + await database_with_data.commit() + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get("/editions/ed2023/students/1") + assert response.status_code == status.HTTP_404_NOT_FOUND -def test_get_students_no_autorization(database_with_data: Session, auth_client: AuthClient): +async def test_get_students_no_autorization(database_with_data: AsyncSession, auth_client: AuthClient): """tests you have to be logged in to get all students""" - assert auth_client.get( - "/editions/ed2022/students/").status_code == status.HTTP_401_UNAUTHORIZED + async with auth_client: + assert (await auth_client.get( + "/editions/ed2022/students/", follow_redirects=True)).status_code == status.HTTP_401_UNAUTHORIZED -def test_get_all_students(database_with_data: Session, auth_client: AuthClient): +async def test_get_all_students(database_with_data: AsyncSession, auth_client: AuthClient, + current_edition: Edition): """tests get all students""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - response = auth_client.get("/editions/ed2022/students/") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()["students"]) == 2 + await auth_client.coach(current_edition) + async with auth_client: + response = await auth_client.get("/editions/ed2022/students/", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == 2 -def test_get_all_students_pagination(database_with_data: Session, auth_client: AuthClient): +async def test_get_all_students_pagination(database_with_data: AsyncSession, auth_client: AuthClient): """tests get all students with pagination""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) for i in range(round(DB_PAGE_SIZE * 1.5)): student: Student = Student(first_name=f"Student {i}", last_name="Vermeulen", preferred_name=f"{i}", email_address=f"student{i}@mail.com", phone_number=f"0487/0{i}.24.45", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[]) database_with_data.add(student) - database_with_data.commit() - response = auth_client.get("/editions/ed2022/students/?page=0") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['students']) == DB_PAGE_SIZE - response = auth_client.get("/editions/ed2022/students/?page=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['students']) == max( - round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + 2, 0) # +2 because there were already 2 students in the database - - -def test_get_first_name_students(database_with_data: Session, auth_client: AuthClient): + await database_with_data.commit() + async with auth_client: + response = await auth_client.get("/editions/ed2022/students?page=0", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['students']) == DB_PAGE_SIZE + response = await auth_client.get("/editions/ed2022/students?page=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['students']) == max( + round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + 2, 0) # +2 because there were already 2 students in the database + + +async def test_get_first_name_students(database_with_data: AsyncSession, auth_client: AuthClient): """tests get students based on query paramer first name""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - response = auth_client.get("/editions/ed2022/students/?name=Jos") + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get("/editions/ed2022/students?name=Jos", follow_redirects=True) assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 1 -def test_get_first_name_student_pagination(database_with_data: Session, auth_client: AuthClient): +async def test_get_first_name_student_pagination(database_with_data: AsyncSession, auth_client: AuthClient): """tests get students based on query paramer first name with pagination""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) for i in range(round(DB_PAGE_SIZE * 1.5)): student: Student = Student(first_name=f"Student {i}", last_name="Vermeulen", preferred_name=f"{i}", email_address=f"student{i}@mail.com", phone_number=f"0487/0{i}.24.45", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[]) database_with_data.add(student) - database_with_data.commit() - response = auth_client.get( - "/editions/ed2022/students/?name=Student&page=0") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()["students"]) == DB_PAGE_SIZE - response = auth_client.get( - "/editions/ed2022/students/?name=Student&page=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['students']) == max( - round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE, 0) - - -def test_get_last_name_students(database_with_data: Session, auth_client: AuthClient): + await database_with_data.commit() + async with auth_client: + response = await auth_client.get( + "/editions/ed2022/students?name=Student&page=0", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == DB_PAGE_SIZE + response = await auth_client.get( + "/editions/ed2022/students?name=Student&page=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['students']) == max( + round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE, 0) + + +async def test_get_last_name_students(database_with_data: AsyncSession, auth_client: AuthClient): """tests get students based on query paramer last name""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - response = auth_client.get( - "/editions/ed2022/students/?name=Vermeulen") + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get( + "/editions/ed2022/students?name=Vermeulen", follow_redirects=True) assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 1 -def test_get_last_name_students_pagination(database_with_data: Session, auth_client: AuthClient): +async def test_get_last_name_students_pagination(database_with_data: AsyncSession, auth_client: AuthClient): """tests get students based on query paramer last name with pagination""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) for i in range(round(DB_PAGE_SIZE * 1.5)): student: Student = Student(first_name="Jos", last_name=f"Student {i}", preferred_name=f"{i}", email_address=f"student{i}@mail.com", phone_number=f"0487/0{i}.24.45", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[]) database_with_data.add(student) - database_with_data.commit() - response = auth_client.get( - "/editions/ed2022/students/?name=Student&page=0") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()["students"]) == DB_PAGE_SIZE - response = auth_client.get( - "/editions/ed2022/students/?name=Student&page=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['students']) == max( - round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE, 0) - - -def test_get_between_first_and_last_name_students(database_with_data: Session, auth_client: AuthClient): + await database_with_data.commit() + async with auth_client: + response = await auth_client.get( + "/editions/ed2022/students?name=Student&page=0", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == DB_PAGE_SIZE + response = await auth_client.get( + "/editions/ed2022/students?name=Student&page=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['students']) == max( + round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE, 0) + + +async def test_get_between_first_and_last_name_students(database_with_data: AsyncSession, auth_client: AuthClient): """tests get students based on query paramer first- and last name""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - response = auth_client.get( - "/editions/ed2022/students/?name=os V") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()["students"]) == 1 + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get( + "/editions/ed2022/students?name=os V", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == 1 -def test_get_alumni_students(database_with_data: Session, auth_client: AuthClient): +async def test_get_alumni_students(database_with_data: AsyncSession, auth_client: AuthClient): """tests get students based on query paramer alumni""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - response = auth_client.get("/editions/ed2022/students/?alumni=true") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()["students"]) == 1 + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get("/editions/ed2022/students?alumni=true", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == 1 -def test_get_alumni_students_pagination(database_with_data: Session, auth_client: AuthClient): +async def test_get_alumni_students_pagination(database_with_data: AsyncSession, auth_client: AuthClient): """tests get students based on query paramer alumni with pagination""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) for i in range(round(DB_PAGE_SIZE * 1.5)): student: Student = Student(first_name="Jos", last_name=f"Student {i}", preferred_name=f"{i}", email_address=f"student{i}@mail.com", phone_number=f"0487/0{i}.24.45", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[]) database_with_data.add(student) - database_with_data.commit() - response = auth_client.get( - "/editions/ed2022/students/?alumni=true&page=0") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()["students"]) == DB_PAGE_SIZE - response = auth_client.get( - "/editions/ed2022/students/?alumni=true&page=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['students']) == max( - round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + 1, 0) # +1 because there is already is one - - -def test_get_student_coach_students(database_with_data: Session, auth_client: AuthClient): + await database_with_data.commit() + async with auth_client: + response = await auth_client.get( + "/editions/ed2022/students?alumni=true&page=0", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == DB_PAGE_SIZE + response = await auth_client.get( + "/editions/ed2022/students?alumni=true&page=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['students']) == max( + round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + 1, 0) # +1 because there is already is one + + +async def test_get_student_coach_students(database_with_data: AsyncSession, auth_client: AuthClient): """tests get students based on query paramer student coach""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - response = auth_client.get("/editions/ed2022/students/?student_coach=true") + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get("/editions/ed2022/students?student_coach=true", follow_redirects=True) assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 1 -def test_get_student_coach_students_pagination(database_with_data: Session, auth_client: AuthClient): +async def test_get_student_coach_students_pagination(database_with_data: AsyncSession, auth_client: AuthClient): """tests get students based on query paramer student coach with pagination""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) for i in range(round(DB_PAGE_SIZE * 1.5)): student: Student = Student(first_name="Jos", last_name=f"Student {i}", preferred_name=f"{i}", email_address=f"student{i}@mail.com", phone_number=f"0487/0{i}.24.45", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[]) database_with_data.add(student) - database_with_data.commit() - response = auth_client.get( - "/editions/ed2022/students/?student_coach=true&page=0") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()["students"]) == DB_PAGE_SIZE - response = auth_client.get( - "/editions/ed2022/students/?student_coach=true&page=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['students']) == max( - round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + 1, 0) # +1 because there is already is one - - -def test_get_one_skill_students(database_with_data: Session, auth_client: AuthClient): + await database_with_data.commit() + async with auth_client: + response = await auth_client.get( + "/editions/ed2022/students?student_coach=true&page=0", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == DB_PAGE_SIZE + response = await auth_client.get( + "/editions/ed2022/students?student_coach=true&page=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['students']) == max( + round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + 1, 0) # +1 because there is already is one + + +async def test_get_one_skill_students(database_with_data: AsyncSession, auth_client: AuthClient): """tests get students based on query paramer one skill""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - response = auth_client.get("/editions/ed2022/students/?skill_ids=1") + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get("/editions/ed2022/students?skill_ids=1", follow_redirects=True) assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 1 assert response.json()["students"][0]["firstName"] == "Jos" -def test_get_multiple_skill_students(database_with_data: Session, auth_client: AuthClient): +async def test_get_multiple_skill_students(database_with_data: AsyncSession, auth_client: AuthClient): """tests get students based on query paramer multiple skills""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - response = auth_client.get( - "/editions/ed2022/students/?skill_ids=4&skill_ids=5") + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get( + "/editions/ed2022/students?skill_ids=4&skill_ids=5", follow_redirects=True) assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 1 assert response.json()["students"][0]["firstName"] == "Marta" -def test_get_multiple_skill_students_no_students(database_with_data: Session, auth_client: AuthClient): +async def test_get_multiple_skill_students_no_students(database_with_data: AsyncSession, auth_client: AuthClient): """tests get students based on query paramer multiple skills, but that student don't excist""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - response = auth_client.get( - "/editions/ed2022/students/?skill_ids=4&skill_ids=6") + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get( + "/editions/ed2022/students?skill_ids=4&skill_ids=6", follow_redirects=True) assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 0 -def test_get_ghost_skill_students(database_with_data: Session, auth_client: AuthClient): +async def test_get_ghost_skill_students(database_with_data: AsyncSession, auth_client: AuthClient): """tests get students based on query paramer one skill that don't excist""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - response = auth_client.get("/editions/ed2022/students/?skill_ids=100") + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get("/editions/ed2022/students?skill_ids=100", follow_redirects=True) assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 0 -def test_get_one_real_one_ghost_skill_students(database_with_data: Session, auth_client: AuthClient): +async def test_get_one_real_one_ghost_skill_students(database_with_data: AsyncSession, auth_client: AuthClient): """tests get students based on query paramer one skill that excist and one that don't excist""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - response = auth_client.get( - "/editions/ed2022/students/?skill_ids=4&skill_ids=100") + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get( + "/editions/ed2022/students?skill_ids=4&skill_ids=100", follow_redirects=True) assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 0 -def test_get_emails_student_no_authorization(database_with_data: Session, auth_client: AuthClient): +async def test_get_students_filter_decisions_one(database_with_data: AsyncSession, auth_client: AuthClient): + """tests get students based on query parameter decisions""" + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get( + "/editions/ed2022/students?decisions=0", follow_redirects=True) + print(response.content) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == 1 + + +async def test_get_students_filter_decisions_multiple(database_with_data: AsyncSession, auth_client: AuthClient): + """tests get students based on multiple decisions""" + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get( + "/editions/ed2022/students?decisions=0&decisions=1", follow_redirects=True) + print(response.content) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == 2 + + +async def test_get_students_own_suggestion(database_with_data: AsyncSession, auth_client: AuthClient): + """test get student based on query paramter for getting the students you wrote a suggestion for""" + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + await auth_client.post("/editions/ed2022/students/2/suggestions", + json={"suggestion": 1, "argumentation": "test"}) + response = await auth_client.get( + "/editions/ed2022/students?own_suggestions=true", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == 1 + assert response.json()["students"][0]["studentId"] == 2 + + +async def test_get_emails_student_no_authorization(database_with_data: AsyncSession, auth_client: AuthClient): """tests that you can't get the mails of a student when you aren't logged in""" - response = auth_client.get("/editions/ed2022/students/1/emails") + async with auth_client: + response = await auth_client.get("/editions/ed2022/students/1/emails") assert response.status_code == status.HTTP_401_UNAUTHORIZED -def test_get_emails_student_coach(database_with_data: Session, auth_client: AuthClient): +async def test_get_emails_student_coach(database_with_data: AsyncSession, auth_client: AuthClient): """tests that a coach can't get the mails of a student""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - response = auth_client.get("/editions/ed2022/students/1/emails") + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.get("/editions/ed2022/students/1/emails") assert response.status_code == status.HTTP_403_FORBIDDEN -def test_get_emails_student_admin(database_with_data: Session, auth_client: AuthClient): +async def test_get_emails_student_admin(database_with_data: AsyncSession, auth_client: AuthClient): """tests that an admin can get the mails of a student""" - auth_client.admin() - auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [1], "email_status": 1}) - response = auth_client.get("/editions/ed2022/students/1/emails") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()["emails"]) == 1 - assert response.json()["student"]["studentId"] == 1 - response = auth_client.get("/editions/ed2022/students/2/emails") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()["emails"]) == 0 - assert response.json()["student"]["studentId"] == 2 - - -def test_post_email_no_authorization(database_with_data: Session, auth_client: AuthClient): + await auth_client.admin() + async with auth_client: + await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [1], "email_status": 1}) + response = await auth_client.get("/editions/ed2022/students/1/emails") + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["emails"]) == 1 + assert response.json()["student"]["studentId"] == 1 + response = await auth_client.get("/editions/ed2022/students/2/emails") + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["emails"]) == 0 + assert response.json()["student"]["studentId"] == 2 + + +async def test_post_email_no_authorization(database_with_data: AsyncSession, auth_client: AuthClient): """tests user need to be loged in""" - response = auth_client.post("/editions/ed2022/students/emails") + async with auth_client: + response = await auth_client.post("/editions/ed2022/students/emails") assert response.status_code == status.HTTP_401_UNAUTHORIZED -def test_post_email_coach(database_with_data: Session, auth_client: AuthClient): +async def test_post_email_coach(database_with_data: AsyncSession, auth_client: AuthClient): """tests user can't be a coach""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - response = auth_client.post("/editions/ed2022/students/emails") + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.post("/editions/ed2022/students/emails") assert response.status_code == status.HTTP_403_FORBIDDEN -def test_post_email_applied(database_with_data: Session, auth_client: AuthClient): +async def test_post_email_applied(database_with_data: AsyncSession, auth_client: AuthClient): """test create email applied""" - auth_client.admin() - response = auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [2], "email_status": 0}) + await auth_client.admin() + async with auth_client: + response = await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [2], "email_status": 0}) assert response.status_code == status.HTTP_201_CREATED assert EmailStatusEnum( response.json()["studentEmails"][0]["emails"][0]["decision"]) == EmailStatusEnum.APPLIED -def test_post_email_awaiting_project(database_with_data: Session, auth_client: AuthClient): +async def test_post_email_awaiting_project(database_with_data: AsyncSession, auth_client: AuthClient): """test create email awaiting project""" - auth_client.admin() - response = auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [2], "email_status": 1}) + await auth_client.admin() + async with auth_client: + response = await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [2], "email_status": 1}) assert response.status_code == status.HTTP_201_CREATED assert EmailStatusEnum( response.json()["studentEmails"][0]["emails"][0]["decision"]) == EmailStatusEnum.AWAITING_PROJECT -def test_post_email_approved(database_with_data: Session, auth_client: AuthClient): +async def test_post_email_approved(database_with_data: AsyncSession, auth_client: AuthClient): """test create email applied""" - auth_client.admin() - response = auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [2], "email_status": 2}) + await auth_client.admin() + async with auth_client: + response = await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [2], "email_status": 2}) assert response.status_code == status.HTTP_201_CREATED assert EmailStatusEnum( response.json()["studentEmails"][0]["emails"][0]["decision"]) == EmailStatusEnum.APPROVED -def test_post_email_contract_confirmed(database_with_data: Session, auth_client: AuthClient): +async def test_post_email_contract_confirmed(database_with_data: AsyncSession, auth_client: AuthClient): """test create email contract confirmed""" - auth_client.admin() - response = auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [2], "email_status": 3}) + await auth_client.admin() + async with auth_client: + response = await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [2], "email_status": 3}) assert response.status_code == status.HTTP_201_CREATED assert EmailStatusEnum( response.json()["studentEmails"][0]["emails"][0]["decision"]) == EmailStatusEnum.CONTRACT_CONFIRMED -def test_post_email_contract_declined(database_with_data: Session, auth_client: AuthClient): +async def test_post_email_contract_declined(database_with_data: AsyncSession, auth_client: AuthClient): """test create email contract declined""" - auth_client.admin() - response = auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [2], "email_status": 4}) + await auth_client.admin() + async with auth_client: + response = await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [2], "email_status": 4}) assert response.status_code == status.HTTP_201_CREATED assert EmailStatusEnum( response.json()["studentEmails"][0]["emails"][0]["decision"]) == EmailStatusEnum.CONTRACT_DECLINED -def test_post_email_rejected(database_with_data: Session, auth_client: AuthClient): +async def test_post_email_rejected(database_with_data: AsyncSession, auth_client: AuthClient): """test create email rejected""" - auth_client.admin() - response = auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [2], "email_status": 5}) + await auth_client.admin() + async with auth_client: + response = await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [2], "email_status": 5}) assert response.status_code == status.HTTP_201_CREATED - print(response.json()) assert EmailStatusEnum( response.json()["studentEmails"][0]["emails"][0]["decision"]) == EmailStatusEnum.REJECTED -def test_creat_email_for_ghost(database_with_data: Session, auth_client: AuthClient): +async def test_creat_email_for_ghost(database_with_data: AsyncSession, auth_client: AuthClient): """test create email for student that don't exist""" - auth_client.admin() - response = auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [100], "email_status": 5}) + await auth_client.admin() + async with auth_client: + response = await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [100], "email_status": 5}) assert response.status_code == status.HTTP_404_NOT_FOUND -def test_creat_email_student_in_other_edition(database_with_data: Session, auth_client: AuthClient): - """test creat an email for a student not in this edition""" +async def test_create_email_student_in_other_edition_bulk(database_with_data: AsyncSession, auth_client: AuthClient): + """test creating an email for a student not in this edition when sending them in bulk + The expected result is that only the mails to students in that edition are sent, and the + others are ignored + """ edition: Edition = Edition(year=2023, name="ed2023") database_with_data.add(edition) student: Student = Student(first_name="Mehmet", last_name="Dizdar", preferred_name="Mehmet", email_address="mehmet.dizdar@example.com", phone_number="(787)-938-6216", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[]) database_with_data.add(student) - database_with_data.commit() - auth_client.admin() - response = auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [3], "email_status": 5}) - print(response.json()) - assert response.status_code == status.HTTP_201_CREATED - assert len(response.json()["studentEmails"]) == 0 + await database_with_data.commit() + await auth_client.admin() + async with auth_client: + response = await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [1, student.student_id], "email_status": 5}) + + # When sending a request for students that aren't in this edition, + # it ignores them & creates emails for the rest instead + assert response.status_code == status.HTTP_201_CREATED + assert len(response.json()["studentEmails"]) == 1 + assert response.json()["studentEmails"][0]["student"]["studentId"] == 1 + + +async def test_create_emails_readonly_edition(database_session: AsyncSession, auth_client: AuthClient): + """Test sending emails in a readonly edition""" + edition: Edition = Edition(year=2023, name="ed2023", readonly=True) + database_session.add(edition) + student: Student = Student(first_name="Mehmet", last_name="Dizdar", preferred_name="Mehmet", + email_address="mehmet.dizdar@example.com", phone_number="(787)-938-6216", alumni=True, + wants_to_be_student_coach=False, edition=edition, skills=[]) + database_session.add(student) + await database_session.commit() + await auth_client.admin() + + async with auth_client: + response = await auth_client.post(f"/editions/{edition.name}/students/emails", + json={"students_id": [student.student_id], "email_status": 5}) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED -def test_get_emails_no_authorization(database_with_data: Session, auth_client: AuthClient): +async def test_get_emails_no_authorization(database_with_data: AsyncSession, auth_client: AuthClient): """test get emails not loged in""" - response = auth_client.get("/editions/ed2022/students/emails") + async with auth_client: + response = await auth_client.get("/editions/ed2022/students/emails") assert response.status_code == status.HTTP_401_UNAUTHORIZED -def test_get_emails_coach(database_with_data: Session, auth_client: AuthClient): +async def test_get_emails_coach(database_with_data: AsyncSession, auth_client: AuthClient): """test get emails logged in as coach""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - response = auth_client.post("/editions/ed2022/students/emails") + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + response = await auth_client.post("/editions/ed2022/students/emails") assert response.status_code == status.HTTP_403_FORBIDDEN -def test_get_emails(database_with_data: Session, auth_client: AuthClient): +async def test_get_emails(database_with_data: AsyncSession, auth_client: AuthClient): """test get emails""" - auth_client.admin() - response = auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [1], "email_status": 3}) - auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [2], "email_status": 5}) - response = auth_client.get("/editions/ed2022/students/emails") + await auth_client.admin() + async with auth_client: + await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [1], "email_status": 3}) + await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [2], "email_status": 5}) + response = await auth_client.get("/editions/ed2022/students/emails") assert response.status_code == status.HTTP_200_OK assert len(response.json()["studentEmails"]) == 2 assert response.json()["studentEmails"][0]["student"]["studentId"] == 1 @@ -541,42 +661,45 @@ def test_get_emails(database_with_data: Session, auth_client: AuthClient): assert response.json()["studentEmails"][1]["emails"][0]["decision"] == 5 -def test_emails_filter_first_name(database_with_data: Session, auth_client: AuthClient): +async def test_emails_filter_first_name(database_with_data: AsyncSession, auth_client: AuthClient): """test get emails with filter first name""" - auth_client.admin() - auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [1], "email_status": 1}) - auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [2], "email_status": 1}) - response = auth_client.get( - "/editions/ed2022/students/emails/?name=Jos") + await auth_client.admin() + async with auth_client: + await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [1], "email_status": 1}) + await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [2], "email_status": 1}) + response = await auth_client.get( + "/editions/ed2022/students/emails?name=Jos", follow_redirects=True) assert len(response.json()["studentEmails"]) == 1 assert response.json()["studentEmails"][0]["student"]["firstName"] == "Jos" -def test_emails_filter_last_name(database_with_data: Session, auth_client: AuthClient): +async def test_emails_filter_last_name(database_with_data: AsyncSession, auth_client: AuthClient): """test get emails with filter last name""" - auth_client.admin() - auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [1], "email_status": 1}) - auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [2], "email_status": 1}) - response = auth_client.get( - "/editions/ed2022/students/emails/?name=Vermeulen") + await auth_client.admin() + async with auth_client: + await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [1], "email_status": 1}) + await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [2], "email_status": 1}) + response = await auth_client.get( + "/editions/ed2022/students/emails?name=Vermeulen", follow_redirects=True) assert len(response.json()["studentEmails"]) == 1 assert response.json()[ "studentEmails"][0]["student"]["lastName"] == "Vermeulen" -def test_emails_filter_between_first_and_last_name(database_with_data: Session, auth_client: AuthClient): +async def test_emails_filter_between_first_and_last_name(database_with_data: AsyncSession, auth_client: AuthClient): """test get emails with filter last name""" - auth_client.admin() - auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [1], "email_status": 1}) - auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [2], "email_status": 1}) - response = auth_client.get( - "/editions/ed2022/students/emails/?name=os V") + await auth_client.admin() + async with auth_client: + await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [1], "email_status": 1}) + await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [2], "email_status": 1}) + response = await auth_client.get( + "/editions/ed2022/students/emails?name=os V", follow_redirects=True) assert len(response.json()["studentEmails"]) == 1 assert response.json()[ "studentEmails"][0]["student"]["firstName"] == "Jos" @@ -584,31 +707,32 @@ def test_emails_filter_between_first_and_last_name(database_with_data: Session, "studentEmails"][0]["student"]["lastName"] == "Vermeulen" -def test_emails_filter_emailstatus(database_with_data: Session, auth_client: AuthClient): +async def test_emails_filter_emailstatus(database_with_data: AsyncSession, auth_client: AuthClient): """test to get all email status, and you only filter on the email send""" - auth_client.admin() - for i in range(0, 6): - auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [2], "email_status": i}) - response = auth_client.get( - f"/editions/ed2022/students/emails/?email_status={i}") - print(response.json()) - assert len(response.json()["studentEmails"]) == 1 - if i > 0: - response = auth_client.get( - f"/editions/ed2022/students/emails/?email_status={i-1}") - assert len(response.json()["studentEmails"]) == 0 - - -def test_emails_filter_emailstatus_multiple_status(database_with_data: Session, auth_client: AuthClient): + await auth_client.admin() + async with auth_client: + for i in range(0, 6): + await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [2], "email_status": i}) + response = await auth_client.get( + f"/editions/ed2022/students/emails?email_status={i}", follow_redirects=True) + assert len(response.json()["studentEmails"]) == 1 + if i > 0: + response = await auth_client.get( + f"/editions/ed2022/students/emails?email_status={i - 1}", follow_redirects=True) + assert len(response.json()["studentEmails"]) == 0 + + +async def test_emails_filter_emailstatus_multiple_status(database_with_data: AsyncSession, auth_client: AuthClient): """test to get all email status with multiple status""" - auth_client.admin() - auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [2], "email_status": 1}) - auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [1], "email_status": 3}) - response = auth_client.get( - "/editions/ed2022/students/emails/?email_status=3&email_status=1") + await auth_client.admin() + async with auth_client: + await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [2], "email_status": 1}) + await auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [1], "email_status": 3}) + response = await auth_client.get( + "/editions/ed2022/students/emails?email_status=3&email_status=1", follow_redirects=True) assert len(response.json()["studentEmails"]) == 2 assert response.json()["studentEmails"][0]["student"]["studentId"] == 1 assert response.json()["studentEmails"][1]["student"]["studentId"] == 2 diff --git a/backend/tests/test_routers/test_editions/test_students/test_suggestions/test_suggestions.py b/backend/tests/test_routers/test_editions/test_students/test_suggestions/test_suggestions.py index 487af0c5c..debdc7389 100644 --- a/backend/tests/test_routers/test_editions/test_students/test_suggestions/test_suggestions.py +++ b/backend/tests/test_routers/test_editions/test_students/test_suggestions/test_suggestions.py @@ -1,5 +1,6 @@ import pytest -from sqlalchemy.orm import Session +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status from src.database.enums import DecisionEnum from src.database.models import Suggestion, Student, User, Edition, Skill @@ -8,33 +9,33 @@ @pytest.fixture -def database_with_data(database_session: Session) -> Session: +async def database_with_data(database_session: AsyncSession) -> AsyncSession: """A fixture to fill the database with fake data that can easly be used when testing""" # Editions edition: Edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() # Users coach1: User = User(name="coach1", editions=[edition]) database_session.add(coach1) - database_session.commit() + await database_session.commit() # Skill - skill1: Skill = Skill(name="skill1", description="something about skill1") - skill2: Skill = Skill(name="skill2", description="something about skill2") - skill3: Skill = Skill(name="skill3", description="something about skill3") - skill4: Skill = Skill(name="skill4", description="something about skill4") - skill5: Skill = Skill(name="skill5", description="something about skill5") - skill6: Skill = Skill(name="skill6", description="something about skill6") + skill1: Skill = Skill(name="skill1") + skill2: Skill = Skill(name="skill2") + skill3: Skill = Skill(name="skill3") + skill4: Skill = Skill(name="skill4") + skill5: Skill = Skill(name="skill5") + skill6: Skill = Skill(name="skill6") database_session.add(skill1) database_session.add(skill2) database_session.add(skill3) database_session.add(skill4) database_session.add(skill5) database_session.add(skill6) - database_session.commit() + await database_session.commit() # Student student01: Student = Student(first_name="Jos", last_name="Vermeulen", preferred_name="Joske", @@ -46,198 +47,252 @@ def database_with_data(database_session: Session) -> Session: database_session.add(student01) database_session.add(student30) - database_session.commit() + await database_session.commit() # Suggestion suggestion1: Suggestion = Suggestion( student=student01, coach=coach1, argumentation="Good student", suggestion=DecisionEnum.YES) database_session.add(suggestion1) - database_session.commit() + await database_session.commit() return database_session -def test_new_suggestion(database_with_data: Session, auth_client: AuthClient): +async def test_new_suggestion(database_with_data: AsyncSession, auth_client: AuthClient): """Tests creating a new suggestion""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - resp = auth_client.post("/editions/ed2022/students/2/suggestions/", - json={"suggestion": 1, "argumentation": "test"}) - assert resp.status_code == status.HTTP_201_CREATED - suggestions: list[Suggestion] = database_with_data.query( - Suggestion).where(Suggestion.student_id == 2).all() + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + resp = await auth_client.post("/editions/ed2022/students/2/suggestions", + json={"suggestion": 1, "argumentation": "test"}) + assert resp.status_code == status.HTTP_201_CREATED + suggestions: list[Suggestion] = (await database_with_data.execute(select( + Suggestion).where(Suggestion.student_id == 2))).unique().scalars().all() assert len(suggestions) == 1 assert DecisionEnum(resp.json()["suggestion"] ["suggestion"]) == suggestions[0].suggestion assert resp.json()[ - "suggestion"]["argumentation"] == suggestions[0].argumentation + "suggestion"]["argumentation"] == suggestions[0].argumentation -def test_overwrite_suggestion(database_with_data: Session, auth_client: AuthClient): - """Tests that when you've already made a suggestion earlier, the existing one is replaced""" - # Create initial suggestion - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - auth_client.post("/editions/ed2022/students/2/suggestions/", - json={"suggestion": 1, "argumentation": "test"}) +async def test_new_suggestion_readonly_edition(database_session: AsyncSession, auth_client: AuthClient): + """Tests creating a new suggestion when the edition is read-only""" + edition = Edition(year=2022, name="ed2022", readonly=True) + await auth_client.admin() - suggestions: list[Suggestion] = database_with_data.query( - Suggestion).where(Suggestion.student_id == 2).all() - assert len(suggestions) == 1 + student: Student = Student(first_name="Marta", last_name="Marquez", preferred_name="Marta", + email_address="marta.marquez@example.com", phone_number="967-895-285", alumni=False, + decision=DecisionEnum.YES, wants_to_be_student_coach=False, edition=edition, + skills=[]) - # Send a new request - arg = "overwritten" - resp = auth_client.post("/editions/ed2022/students/2/suggestions/", - json={"suggestion": 2, "argumentation": arg}) - assert resp.status_code == status.HTTP_201_CREATED - suggestions: list[Suggestion] = database_with_data.query( - Suggestion).where(Suggestion.student_id == 2).all() - assert len(suggestions) == 1 - assert suggestions[0].argumentation == arg + database_session.add(edition) + database_session.add(student) + await database_session.commit() + async with auth_client: + response = await auth_client.post(f"/editions/{edition.name}/students/{student.student_id}/suggestions", + json={"suggestion": 1, "argumentation": "test"}) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED -def test_new_suggestion_not_authorized(database_with_data: Session, auth_client: AuthClient): - """Tests when not authorized you can't add a new suggestion""" - assert auth_client.post("/editions/ed2022/students/2/suggestions/", json={ - "suggestion": 1, "argumentation": "test"}).status_code == status.HTTP_401_UNAUTHORIZED - suggestions: list[Suggestion] = database_with_data.query( - Suggestion).where(Suggestion.student_id == 2).all() - assert len(suggestions) == 0 +async def test_overwrite_suggestion(database_with_data: AsyncSession, auth_client: AuthClient): + """Tests that when you've already made a suggestion earlier, the existing one is replaced""" + # Create initial suggestion + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + await auth_client.post("/editions/ed2022/students/2/suggestions", + json={"suggestion": 1, "argumentation": "test"}) + + suggestions: list[Suggestion] = (await database_with_data.execute(select( + Suggestion).where(Suggestion.student_id == 2))).unique().scalars().all() + assert len(suggestions) == 1 + + # Send a new request + arg = "overwritten" + resp = await auth_client.post("/editions/ed2022/students/2/suggestions", + json={"suggestion": 2, "argumentation": arg}) + assert resp.status_code == status.HTTP_201_CREATED + suggestions: list[Suggestion] = (await database_with_data.execute(select( + Suggestion).where(Suggestion.student_id == 2))).unique().scalars().all() + assert len(suggestions) == 1 + assert suggestions[0].argumentation == arg + + +async def test_new_suggestion_not_authorized(database_with_data: AsyncSession, auth_client: AuthClient): + """Tests when not authorized you can't add a new suggestion""" + async with auth_client: + assert (await auth_client.post("/editions/ed2022/students/2/suggestions", json={ + "suggestion": 1, "argumentation": "test"})).status_code == status.HTTP_401_UNAUTHORIZED + suggestions: list[Suggestion] = (await database_with_data.execute(select( + Suggestion).where(Suggestion.student_id == 2))).unique().scalars().all() + assert len(suggestions) == 0 -def test_get_suggestions_of_student_not_authorized(database_with_data: Session, auth_client: AuthClient): +async def test_get_suggestions_of_student_not_authorized(database_with_data: AsyncSession, auth_client: AuthClient): """Tests if you don't have the right access, you get the right HTTP code""" - - assert auth_client.get("/editions/ed2022/students/29/suggestions/", headers={"Authorization": "auth"}, json={ - "suggestion": 1, "argumentation": "Ja"}).status_code == status.HTTP_401_UNAUTHORIZED + async with auth_client: + assert (await auth_client.get("/editions/ed2022/students/29/suggestions", headers={"Authorization": "auth"} + )).status_code == status.HTTP_401_UNAUTHORIZED -def test_get_suggestions_of_ghost(database_with_data: Session, auth_client: AuthClient): +async def test_get_suggestions_of_ghost(database_with_data: AsyncSession, auth_client: AuthClient): """Tests if the student don't exist, you get a 404""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - res = auth_client.get( - "/editions/ed2022/students/9000/suggestions/") - assert res.status_code == status.HTTP_404_NOT_FOUND + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + res = await auth_client.get( + "/editions/ed2022/students/9000/suggestions") + assert res.status_code == status.HTTP_404_NOT_FOUND -def test_get_suggestions_of_student(database_with_data: Session, auth_client: AuthClient): +async def test_get_suggestions_of_student(database_with_data: AsyncSession, auth_client: AuthClient): """Tests to get the suggestions of a student""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - assert auth_client.post("/editions/ed2022/students/2/suggestions/", json={ - "suggestion": 1, "argumentation": "Ja"}).status_code == status.HTTP_201_CREATED - auth_client.admin() - assert auth_client.post("/editions/ed2022/students/2/suggestions/", json={ - "suggestion": 3, "argumentation": "Neen"}).status_code == status.HTTP_201_CREATED - res = auth_client.get( - "/editions/1/students/2/suggestions/") - assert res.status_code == status.HTTP_200_OK - res_json = res.json() - assert len(res_json["suggestions"]) == 2 - assert res_json["suggestions"][0]["suggestion"] == 1 - assert res_json["suggestions"][0]["argumentation"] == "Ja" - assert res_json["suggestions"][1]["suggestion"] == 3 - assert res_json["suggestions"][1]["argumentation"] == "Neen" - - -def test_delete_ghost_suggestion(database_with_data: Session, auth_client: AuthClient): + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + assert (await auth_client.post("/editions/ed2022/students/2/suggestions", json={ + "suggestion": 1, "argumentation": "Ja"})).status_code == status.HTTP_201_CREATED + await auth_client.admin() + assert (await auth_client.post("/editions/ed2022/students/2/suggestions", json={ + "suggestion": 3, "argumentation": "Neen"})).status_code == status.HTTP_201_CREATED + res = await auth_client.get( + "/editions/ed2022/students/2/suggestions") + assert res.status_code == status.HTTP_200_OK + res_json = res.json() + assert len(res_json["suggestions"]) == 2 + assert res_json["suggestions"][0]["suggestion"] == 1 + assert res_json["suggestions"][0]["argumentation"] == "Ja" + assert res_json["suggestions"][1]["suggestion"] == 3 + assert res_json["suggestions"][1]["argumentation"] == "Neen" + + +async def test_delete_ghost_suggestion(database_with_data: AsyncSession, auth_client: AuthClient): """Tests that you get the correct status code when you delete a not existing suggestion""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - assert auth_client.delete( - "/editions/ed2022/students/1/suggestions/8000").status_code == status.HTTP_404_NOT_FOUND + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + assert (await auth_client.delete( + "/editions/ed2022/students/1/suggestions/8000")).status_code == status.HTTP_404_NOT_FOUND -def test_delete_not_autorized(database_with_data: Session, auth_client: AuthClient): - """Tests that you have to be loged in for deleating a suggestion""" - assert auth_client.delete( - "/editions/ed2022/students/1/suggestions/8000").status_code == status.HTTP_401_UNAUTHORIZED +async def test_delete_not_authorized(database_with_data: AsyncSession, auth_client: AuthClient): + """Tests that you have to be logged in in order to delete a suggestion""" + async with auth_client: + assert (await auth_client.delete( + "/editions/ed2022/students/1/suggestions/8000")).status_code == status.HTTP_401_UNAUTHORIZED -def test_delete_suggestion_admin(database_with_data: Session, auth_client: AuthClient): +async def test_delete_suggestion_admin(database_with_data: AsyncSession, auth_client: AuthClient): """Test that an admin can update suggestions""" - auth_client.admin() - assert auth_client.delete( - "/editions/ed2022/students/1/suggestions/1").status_code == status.HTTP_204_NO_CONTENT - suggestions: Suggestion = database_with_data.query( - Suggestion).where(Suggestion.suggestion_id == 1).all() - assert len(suggestions) == 0 + await auth_client.admin() + async with auth_client: + assert (await auth_client.delete( + "/editions/ed2022/students/1/suggestions/1")).status_code == status.HTTP_204_NO_CONTENT + suggestions: list[Suggestion] = (await database_with_data.execute(select( + Suggestion).where(Suggestion.suggestion_id == 1))).unique().scalars().all() + assert len(suggestions) == 0 -def test_delete_suggestion_coach_their_review(database_with_data: Session, auth_client: AuthClient): +async def test_delete_suggestion_coach_their_review(database_with_data: AsyncSession, auth_client: AuthClient): """Tests that a coach can delete their own suggestion""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - new_suggestion = auth_client.post("/editions/ed2022/students/2/suggestions/", - json={"suggestion": 1, "argumentation": "test"}) - assert new_suggestion.status_code == status.HTTP_201_CREATED - suggestion_id = new_suggestion.json()["suggestion"]["suggestionId"] - assert auth_client.delete( - f"/editions/ed2022/students/1/suggestions/{suggestion_id}").status_code == status.HTTP_204_NO_CONTENT - suggestions: Suggestion = database_with_data.query( - Suggestion).where(Suggestion.suggestion_id == suggestion_id).all() - assert len(suggestions) == 0 - - -def test_delete_suggestion_coach_other_review(database_with_data: Session, auth_client: AuthClient): + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + new_suggestion = await auth_client.post("/editions/ed2022/students/2/suggestions", + json={"suggestion": 1, "argumentation": "test"}) + assert new_suggestion.status_code == status.HTTP_201_CREATED + suggestion_id = new_suggestion.json()["suggestion"]["suggestionId"] + assert (await auth_client.delete( + f"/editions/ed2022/students/2/suggestions/{suggestion_id}")).status_code == status.HTTP_204_NO_CONTENT + suggestions: list[Suggestion] = (await database_with_data.execute(select( + Suggestion).where(Suggestion.suggestion_id == suggestion_id))).unique().scalars().all() + assert len(suggestions) == 0 + + +async def test_delete_suggestion_wrong_student(database_with_data: AsyncSession, auth_client: AuthClient): + """Test you can't delete an suggestion that's don't belong to that student""" + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + new_suggestion = await auth_client.post("/editions/ed2022/students/2/suggestions", + json={"suggestion": 1, "argumentation": "test"}) + assert new_suggestion.status_code == status.HTTP_201_CREATED + suggestion_id = new_suggestion.json()["suggestion"]["suggestionId"] + assert (await auth_client.delete( + f"/editions/ed2022/students/1/suggestions/{suggestion_id}")).status_code == status.HTTP_404_NOT_FOUND + res = await auth_client.get( + "/editions/ed2022/students/2/suggestions") + assert res.status_code == status.HTTP_200_OK + res_json = res.json() + assert len(res_json["suggestions"]) == 1 + assert res_json["suggestions"][0]["suggestion"] == 1 + assert res_json["suggestions"][0]["argumentation"] == "test" + + +async def test_delete_suggestion_coach_other_review(database_with_data: AsyncSession, auth_client: AuthClient): """Tests that a coach can't delete other coaches their suggestions""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - assert auth_client.delete( - "/editions/ed2022/students/1/suggestions/1").status_code == status.HTTP_403_FORBIDDEN - suggestions: Suggestion = database_with_data.query( - Suggestion).where(Suggestion.suggestion_id == 1).all() - assert len(suggestions) == 1 + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + assert (await auth_client.delete( + "/editions/ed2022/students/1/suggestions/1")).status_code == status.HTTP_403_FORBIDDEN + suggestions: list[Suggestion] = (await database_with_data.execute(select( + Suggestion).where(Suggestion.suggestion_id == 1))).unique().scalars().all() + assert len(suggestions) == 1 -def test_update_ghost_suggestion(database_with_data: Session, auth_client: AuthClient): +async def test_update_ghost_suggestion(database_with_data: AsyncSession, auth_client: AuthClient): """Tests a suggestion that don't exist """ - auth_client.admin() - assert auth_client.put("/editions/ed2022/students/1/suggestions/8000", json={ - "suggestion": 1, "argumentation": "test"}).status_code == status.HTTP_404_NOT_FOUND + await auth_client.admin() + async with auth_client: + assert (await auth_client.put("/editions/ed2022/students/1/suggestions/8000", json={ + "suggestion": 1, "argumentation": "test"})).status_code == status.HTTP_404_NOT_FOUND -def test_update_not_autorized(database_with_data: Session, auth_client: AuthClient): +async def test_update_not_autorized(database_with_data: AsyncSession, auth_client: AuthClient): """Tests update when not autorized""" - assert auth_client.put("/editions/ed2022/students/1/suggestions/8000", json={ - "suggestion": 1, "argumentation": "test"}).status_code == status.HTTP_401_UNAUTHORIZED + async with auth_client: + assert (await auth_client.put("/editions/ed2022/students/1/suggestions/8000", json={ + "suggestion": 1, "argumentation": "test"})).status_code == status.HTTP_401_UNAUTHORIZED -def test_update_suggestion_admin(database_with_data: Session, auth_client: AuthClient): +async def test_update_suggestion_admin(database_with_data: AsyncSession, auth_client: AuthClient): """Test that an admin can update suggestions""" - auth_client.admin() - assert auth_client.put("/editions/ed2022/students/1/suggestions/1", json={ - "suggestion": 3, "argumentation": "test"}).status_code == status.HTTP_204_NO_CONTENT - suggestion: Suggestion = database_with_data.query( - Suggestion).where(Suggestion.suggestion_id == 1).one() - assert suggestion.suggestion == DecisionEnum.NO - assert suggestion.argumentation == "test" + await auth_client.admin() + async with auth_client: + assert (await auth_client.put("/editions/ed2022/students/1/suggestions/1", json={ + "suggestion": 3, "argumentation": "test"})).status_code == status.HTTP_204_NO_CONTENT + suggestion: Suggestion = (await database_with_data.execute(select( + Suggestion).where(Suggestion.suggestion_id == 1))).unique().scalar_one() + assert suggestion.suggestion == DecisionEnum.NO + assert suggestion.argumentation == "test" -def test_update_suggestion_coach_their_review(database_with_data: Session, auth_client: AuthClient): +async def test_update_suggestion_coach_their_review(database_with_data: AsyncSession, auth_client: AuthClient): """Tests that a coach can update their own suggestion""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - new_suggestion = auth_client.post("/editions/ed2022/students/2/suggestions/", - json={"suggestion": 1, "argumentation": "test"}) - assert new_suggestion.status_code == status.HTTP_201_CREATED - suggestion_id = new_suggestion.json()["suggestion"]["suggestionId"] - assert auth_client.put(f"/editions/ed2022/students/1/suggestions/{suggestion_id}", json={ - "suggestion": 3, "argumentation": "test"}).status_code == status.HTTP_204_NO_CONTENT - suggestion: Suggestion = database_with_data.query( - Suggestion).where(Suggestion.suggestion_id == suggestion_id).one() - assert suggestion.suggestion == DecisionEnum.NO - assert suggestion.argumentation == "test" - - -def test_update_suggestion_coach_other_review(database_with_data: Session, auth_client: AuthClient): + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + new_suggestion = await auth_client.post("/editions/ed2022/students/2/suggestions", + json={"suggestion": 1, "argumentation": "test"}) + assert new_suggestion.status_code == status.HTTP_201_CREATED + suggestion_id = new_suggestion.json()["suggestion"]["suggestionId"] + assert (await auth_client.put(f"/editions/ed2022/students/2/suggestions/{suggestion_id}", json={ + "suggestion": 3, "argumentation": "test"})).status_code == status.HTTP_204_NO_CONTENT + suggestion: Suggestion = (await database_with_data.execute(select( + Suggestion).where(Suggestion.suggestion_id == suggestion_id))).unique().scalar_one() + assert suggestion.suggestion == DecisionEnum.NO + assert suggestion.argumentation == "test" + + +async def test_update_suggestion_coach_other_review(database_with_data: AsyncSession, auth_client: AuthClient): """Tests that a coach can't update other coaches their suggestions""" - edition: Edition = database_with_data.query(Edition).all()[0] - auth_client.coach(edition) - assert auth_client.put("/editions/ed2022/students/1/suggestions/1", json={ - "suggestion": 3, "argumentation": "test"}).status_code == status.HTTP_403_FORBIDDEN - suggestion: Suggestion = database_with_data.query( - Suggestion).where(Suggestion.suggestion_id == 1).one() - assert suggestion.suggestion != DecisionEnum.NO - assert suggestion.argumentation != "test" + edition: Edition = (await database_with_data.execute(select(Edition))).scalars().all()[0] + await auth_client.coach(edition) + async with auth_client: + assert (await auth_client.put("/editions/ed2022/students/1/suggestions/1", json={ + "suggestion": 3, "argumentation": "test"})).status_code == status.HTTP_403_FORBIDDEN + suggestion: Suggestion = (await database_with_data.execute(select( + Suggestion).where(Suggestion.suggestion_id == 1))).unique().scalar_one() + assert suggestion.suggestion != DecisionEnum.NO + assert suggestion.argumentation != "test" diff --git a/backend/tests/test_routers/test_editions/test_webhooks/test_webhooks.py b/backend/tests/test_routers/test_editions/test_webhooks/test_webhooks.py index a75d0a789..d569b97c5 100644 --- a/backend/tests/test_routers/test_editions/test_webhooks/test_webhooks.py +++ b/backend/tests/test_routers/test_editions/test_webhooks/test_webhooks.py @@ -2,46 +2,68 @@ from uuid import UUID import pytest -from fastapi.testclient import TestClient -from sqlalchemy.orm import Session +from httpx import AsyncClient +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status -from src.database.models import Edition, WebhookURL, Student +from src.database.models import Edition, WebhookURL, Student, Skill from tests.utils.authorization import AuthClient from .data import create_webhook_event, WEBHOOK_EVENT_BAD_FORMAT, WEBHOOK_MISSING_QUESTION @pytest.fixture -def edition(database_session: Session) -> Edition: +async def edition(database_session: AsyncSession) -> Edition: edition = Edition(year=2022, name="ed2022") database_session.add(edition) - database_session.commit() + await database_session.commit() return edition @pytest.fixture -def webhook(edition: Edition, database_session: Session) -> WebhookURL: +async def webhook(edition: Edition, database_session: AsyncSession) -> WebhookURL: webhook = WebhookURL(edition=edition) database_session.add(webhook) - database_session.commit() + await database_session.commit() return webhook -def test_new_webhook(auth_client: AuthClient, edition: Edition): - auth_client.admin() - response = auth_client.post(f"/editions/{edition.name}/webhooks/") - assert response.status_code == status.HTTP_201_CREATED - assert 'uuid' in response.json() - assert UUID(response.json()['uuid']) - - -def test_new_webhook_invalid_edition(auth_client: AuthClient, edition: Edition): - auth_client.admin() - response = auth_client.post("/editions/invalid/webhooks/") - assert response.status_code == status.HTTP_404_NOT_FOUND - - -def test_webhook(test_client: TestClient, webhook: WebhookURL, database_session: Session): +@pytest.fixture +async def database_session_skills(database_session: AsyncSession) -> AsyncSession: + """fixture to add skills""" + database_session.add(Skill(name="Front-end developer")) + database_session.add(Skill(name="Back-end developer")) + database_session.add(Skill(name="UX / UI designer")) + database_session.add(Skill(name="Graphic designer")) + database_session.add(Skill(name="Business Modeller")) + database_session.add(Skill(name="Storyteller")) + database_session.add(Skill(name="Marketer")) + database_session.add(Skill(name="Copywriter")) + database_session.add(Skill(name="Video editor")) + database_session.add(Skill(name="Photographer")) + await database_session.commit() + return database_session + + +async def test_new_webhook(auth_client: AuthClient, edition: Edition): + await auth_client.admin() + async with auth_client: + response = await auth_client.post(f"/editions/{edition.name}/webhooks") + assert response.status_code == status.HTTP_201_CREATED + assert 'uuid' in response.json() + assert UUID(response.json()['uuid']) + + +async def test_new_webhook_invalid_edition(auth_client: AuthClient, edition: Edition): + await auth_client.admin() + async with auth_client: + response = await auth_client.post("/editions/invalid/webhooks") + assert response.status_code == status.HTTP_404_NOT_FOUND + + +async def test_webhook(database_session_skills: AsyncSession, test_client: AsyncClient, webhook: WebhookURL): + """test webhook""" + event: dict = create_webhook_event( email_address="test@gmail.com", first_name="Bob", @@ -50,10 +72,11 @@ def test_webhook(test_client: TestClient, webhook: WebhookURL, database_session: wants_to_be_student_coach=False, phone_number="0477002266", ) - response = test_client.post(f"/editions/{webhook.edition.name}/webhooks/{webhook.uuid}", json=event) - assert response.status_code == status.HTTP_201_CREATED + async with test_client: + response = await test_client.post(f"/editions/{webhook.edition.name}/webhooks/{webhook.uuid}", json=event) + assert response.status_code == status.HTTP_201_CREATED - student: Student = database_session.query(Student).first() + student: Student = (await database_session_skills.execute(select(Student))).scalars().first() assert student.edition == webhook.edition assert student.email_address == "test@gmail.com" assert student.first_name == "Bob" @@ -61,63 +84,93 @@ def test_webhook(test_client: TestClient, webhook: WebhookURL, database_session: assert student.preferred_name == "Jhon" assert student.wants_to_be_student_coach is False assert student.phone_number == "0477002266" + assert len(student.skills) > 0 -def test_webhook_bad_format(test_client: TestClient, webhook: WebhookURL): - """Test a badly formatted webhook input""" - response = test_client.post( - f"/editions/{webhook.edition.name}/webhooks/{webhook.uuid}", - json=WEBHOOK_EVENT_BAD_FORMAT +async def test_webhook_unknow_question_type(database_session_skills: AsyncSession, test_client: AsyncClient, webhook: WebhookURL): + """test webhook with unknow question type""" + event: dict = create_webhook_event( + email_address="test@gmail.com", + first_name="Bob", + last_name="Klonck", + preferred_name="Jhon", + wants_to_be_student_coach=False, + phone_number="0477002266", ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + event["data"]["fields"].append({ + "key": "question_wrong", + "label": "This is a test", + "type": "TEST", + "value": "89597a5d-bf59-41d0-88b1-wrong6cee12d", + "options": [] + }) + async with test_client: + response = await test_client.post(f"/editions/{webhook.edition.name}/webhooks/{webhook.uuid}", json=event) + print(response.json()) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + +async def test_webhook_bad_format(test_client: AsyncClient, webhook: WebhookURL): + """Test a badly formatted webhook input""" + async with test_client: + response = await test_client.post( + f"/editions/{webhook.edition.name}/webhooks/{webhook.uuid}", + json=WEBHOOK_EVENT_BAD_FORMAT + ) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY -def test_webhook_duplicate_email(test_client: TestClient, webhook: WebhookURL, mocker): +async def test_webhook_duplicate_email(database_session_skills: AsyncSession, test_client: AsyncClient, webhook: WebhookURL, mocker): """Test entering a duplicate email address""" mocker.patch('builtins.open', new_callable=mock_open()) event: dict = create_webhook_event( email_address="test@gmail.com", ) - response = test_client.post(f"/editions/{webhook.edition.name}/webhooks/{webhook.uuid}", json=event) - assert response.status_code == status.HTTP_201_CREATED + async with test_client: + response = await test_client.post(f"/editions/{webhook.edition.name}/webhooks/{webhook.uuid}", json=event) + assert response.status_code == status.HTTP_201_CREATED - event: dict = create_webhook_event( - email_address="test@gmail.com", - ) - response = test_client.post(f"/editions/{webhook.edition.name}/webhooks/{webhook.uuid}", json=event) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + event: dict = create_webhook_event( + email_address="test@gmail.com", + ) + response = await test_client.post(f"/editions/{webhook.edition.name}/webhooks/{webhook.uuid}", json=event) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY -def test_webhook_duplicate_phone(test_client: TestClient, webhook: WebhookURL, mocker): +async def test_webhook_duplicate_phone(database_session_skills: AsyncSession, test_client: AsyncClient, webhook: WebhookURL, mocker): """Test entering a duplicate phone number""" mocker.patch('builtins.open', new_callable=mock_open()) event: dict = create_webhook_event( phone_number="0477002266", ) - response = test_client.post(f"/editions/{webhook.edition.name}/webhooks/{webhook.uuid}", json=event) - assert response.status_code == status.HTTP_201_CREATED + async with test_client: + response = await test_client.post(f"/editions/{webhook.edition.name}/webhooks/{webhook.uuid}", json=event) + assert response.status_code == status.HTTP_201_CREATED - event: dict = create_webhook_event( - phone_number="0477002266", - ) - response = test_client.post(f"/editions/{webhook.edition.name}/webhooks/{webhook.uuid}", json=event) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + event: dict = create_webhook_event( + phone_number="0477002266", + ) + response = await test_client.post(f"/editions/{webhook.edition.name}/webhooks/{webhook.uuid}", json=event) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY -def test_webhook_missing_question(test_client: TestClient, webhook: WebhookURL, mocker): +async def test_webhook_missing_question(database_session_skills: AsyncSession, test_client: AsyncClient, webhook: WebhookURL, mocker): """Test submitting a form with a question missing""" mocker.patch('builtins.open', new_callable=mock_open()) - response = test_client.post( - f"/editions/{webhook.edition.name}/webhooks/{webhook.uuid}", - json=WEBHOOK_MISSING_QUESTION - ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - - -def test_new_webhook_old_edition(database_session: Session, auth_client: AuthClient, edition: Edition): - database_session.add(Edition(year=2023, name="ed2023")) - database_session.commit() - - auth_client.admin() - response = auth_client.post(f"/editions/{edition.name}/webhooks/") - assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + async with test_client: + response = await test_client.post( + f"/editions/{webhook.edition.name}/webhooks/{webhook.uuid}", + json=WEBHOOK_MISSING_QUESTION + ) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + +async def test_new_webhook_readonly_edition(database_session_skills: AsyncSession, auth_client: AuthClient, edition: Edition): + """Test new webhook to an old edition""" + edition.readonly = True + database_session_skills.add(Edition(year=2023, name="ed2023")) + await database_session_skills.commit() + async with auth_client: + await auth_client.admin() + response = await auth_client.post(f"/editions/{edition.name}/webhooks") + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED diff --git a/backend/tests/test_routers/test_login/test_login.py b/backend/tests/test_routers/test_login/test_login.py index 166098cee..d45ab5a6f 100644 --- a/backend/tests/test_routers/test_login/test_login.py +++ b/backend/tests/test_routers/test_login/test_login.py @@ -1,4 +1,6 @@ from sqlalchemy.orm import Session +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status from starlette.testclient import TestClient @@ -6,17 +8,17 @@ from src.database.models import AuthEmail, User -def test_login_non_existing(test_client: TestClient): +async def test_login_non_existing(test_client: AsyncClient): """Test logging in without an existing account""" form = { "username": "this user", "password": "does not exist" } + async with test_client: + assert (await test_client.post("/login/token/email", data=form)).status_code == status.HTTP_401_UNAUTHORIZED - assert test_client.post("/login/token", data=form).status_code == status.HTTP_401_UNAUTHORIZED - -def test_login_existing(database_session: Session, test_client: TestClient): +async def test_login_existing(database_session: AsyncSession, test_client: AsyncClient): """Test logging in with an existing account""" email = "test@ema.il" password = "password" @@ -25,23 +27,23 @@ def test_login_existing(database_session: Session, test_client: TestClient): user = User(name="test") database_session.add(user) - database_session.commit() + await database_session.commit() auth = AuthEmail(pw_hash=security.get_password_hash(password), email=email) auth.user = user database_session.add(auth) - database_session.commit() + await database_session.commit() # Try to get a token using the credentials for the new user form = { "username": email, "password": password } - - assert test_client.post("/login/token", data=form).status_code == status.HTTP_200_OK + async with test_client: + assert (await test_client.post("/login/token/email", data=form)).status_code == status.HTTP_200_OK -def test_login_existing_wrong_credentials(database_session: Session, test_client: TestClient): +async def test_login_existing_wrong_credentials(database_session: AsyncSession, test_client: AsyncClient): """Test logging in with existing, but wrong credentials""" email = "test@ema.il" password = "password" @@ -50,17 +52,47 @@ def test_login_existing_wrong_credentials(database_session: Session, test_client user = User(name="test") database_session.add(user) - database_session.commit() + await database_session.commit() auth = AuthEmail(pw_hash=security.get_password_hash(password), email=email) auth.user = user database_session.add(auth) - database_session.commit() + await database_session.commit() # Try to get a token using the credentials for the new user form = { "username": email, "password": "another password" } + async with test_client: + assert (await test_client.post("/login/token/email", data=form)).status_code == status.HTTP_401_UNAUTHORIZED + + +async def test_refresh(database_session: AsyncSession, test_client: AsyncClient): + """test refresh token""" + email = "test@ema.il" + password = "password" + + # Create new user & auth entries in db + user = User(name="test") + + database_session.add(user) + await database_session.commit() + + auth = AuthEmail(pw_hash=security.get_password_hash(password), email=email) + auth.user = user + database_session.add(auth) + await database_session.commit() + + # Try to get a token using the credentials for the new user + form = { + "username": email, + "password": password + } + async with test_client: + response_login = await test_client.post("/login/token/email", data=form) + assert response_login.status_code == status.HTTP_200_OK + token:str = response_login.json()["refresh_token"] + response_refresh = await test_client.post("/login/refresh", headers={"Authorization": "Bearer " + token }) + assert response_login.status_code == status.HTTP_200_OK - assert test_client.post("/login/token", data=form).status_code == status.HTTP_401_UNAUTHORIZED diff --git a/backend/tests/test_routers/test_skills/test_skills.py b/backend/tests/test_routers/test_skills/test_skills.py index a4a93717e..46f8b6489 100644 --- a/backend/tests/test_routers/test_skills/test_skills.py +++ b/backend/tests/test_routers/test_skills/test_skills.py @@ -1,69 +1,70 @@ -from json import dumps -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status from src.database.models import Skill from tests.utils.authorization import AuthClient -def test_get_skills(database_session: Session, auth_client: AuthClient): - """Performe tests on getting skills +async def test_get_skills(database_session: AsyncSession, auth_client: AuthClient): + """Performs tests on getting skills Args: database_session (Session): a connection with the database auth_client (AuthClient): a client used to do rest calls """ - auth_client.admin() - skill = Skill(name="Backend", description="Must know react") + await auth_client.admin() + skill = Skill(name="Backend") database_session.add(skill) - database_session.commit() + await database_session.commit() # Make the get request - response = auth_client.get("/skills/") + async with auth_client: + response = await auth_client.get("/skills/", follow_redirects=True) - assert response.status_code == status.HTTP_200_OK - response = response.json() - assert response["skills"][0]["name"] == "Backend" - assert response["skills"][0]["description"] == "Must know react" + assert response.status_code == status.HTTP_200_OK + response = response.json() + assert response["skills"][0]["name"] == "Backend" -def test_create_skill(database_session: Session, auth_client: AuthClient): - """Performe tests on creating skills +async def test_create_skill(database_session: AsyncSession, auth_client: AuthClient): + """Perform tests on creating skills Args: database_session (Session): a connection with the database auth_client (AuthClient): a client used to do rest calls """ - auth_client.admin() + await auth_client.admin() # Make the post request - response = auth_client.post("/skills/", data=dumps({"name": "Backend", "description": "must know react"})) - assert response.status_code == status.HTTP_201_CREATED - assert auth_client.get("/skills/").json()["skills"][0]["name"] == "Backend" - assert auth_client.get("/skills/").json()["skills"][0]["description"] == "must know react" + async with auth_client: + response = await auth_client.post("/skills", json={"name": "Backend"}) + assert response.status_code == status.HTTP_201_CREATED + assert (await auth_client.get("/skills/", follow_redirects=True)).json()["skills"][0]["name"] == "Backend" -def test_delete_skill(database_session: Session, auth_client: AuthClient): - """Performe tests on deleting skills +async def test_delete_skill(database_session: AsyncSession, auth_client: AuthClient): + """Perform tests on deleting skills Args: database_session (Session): a connection with the database auth_client (AuthClient): a client used to do rest calls """ - auth_client.admin() + await auth_client.admin() - skill = Skill(name="Backend", description="Must know react") + skill = Skill(name="Backend") database_session.add(skill) - database_session.commit() - database_session.refresh(skill) + await database_session.commit() + await database_session.refresh(skill) - response = auth_client.delete(f"/skills/{skill.skill_id}") - assert response.status_code == status.HTTP_204_NO_CONTENT + async with auth_client: + response = await auth_client.delete(f"/skills/{skill.skill_id}") + assert response.status_code == status.HTTP_204_NO_CONTENT -def test_delete_skill_non_existing(database_session: Session, auth_client: AuthClient): +async def test_delete_skill_non_existing(database_session: AsyncSession, auth_client: AuthClient): """Delete a skill that doesn't exist""" - auth_client.admin() + await auth_client.admin() - response = auth_client.delete("/skills/1") - assert response.status_code == status.HTTP_404_NOT_FOUND + async with auth_client: + response = await auth_client.delete("/skills/1") + assert response.status_code == status.HTTP_404_NOT_FOUND diff --git a/backend/tests/test_routers/test_users/test_users.py b/backend/tests/test_routers/test_users/test_users.py index 38db1dfcf..f36bb4861 100644 --- a/backend/tests/test_routers/test_users/test_users.py +++ b/backend/tests/test_routers/test_users/test_users.py @@ -1,18 +1,17 @@ -from json import dumps - import pytest -from sqlalchemy.orm import Session +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from starlette import status from settings import DB_PAGE_SIZE from src.database import models -from src.database.models import user_editions, CoachRequest +from src.database.models import user_editions, CoachRequest, Edition, User from tests.utils.authorization import AuthClient @pytest.fixture -def data(database_session: Session) -> dict[str, str | int]: +async def data(database_session: AsyncSession) -> dict[str, str | int]: """Fill database with dummy data""" # Create users user1 = models.User(name="user1", admin=True) @@ -27,16 +26,16 @@ def data(database_session: Session) -> dict[str, str | int]: edition2 = models.Edition(year=2, name="ed2") database_session.add(edition2) - database_session.commit() + await database_session.commit() email_auth1 = models.AuthEmail(user_id=user1.user_id, email="user1@mail.com", pw_hash="HASH1") - github_auth1 = models.AuthGitHub(user_id=user2.user_id, gh_auth_id=123, email="user2@mail.com") + github_auth1 = models.AuthGitHub(user_id=user2.user_id, gh_auth_id=123, email="user2@mail.com", github_user_id=2) database_session.add(email_auth1) database_session.add(github_auth1) - database_session.commit() + await database_session.commit() # Create coach roles - database_session.execute(models.user_editions.insert(), [ + await database_session.execute(models.user_editions.insert(), [ {"user_id": user1.user_id, "edition_id": edition1.edition_id}, {"user_id": user2.user_id, "edition_id": edition1.edition_id}, {"user_id": user2.user_id, "edition_id": edition2.edition_id} @@ -53,191 +52,202 @@ def data(database_session: Session) -> dict[str, str | int]: } -def test_get_all_users(database_session: Session, auth_client: AuthClient, data: dict[str, str | int]): +async def test_get_all_users(database_session: AsyncSession, auth_client: AuthClient, data: dict[str, str | int]): """Test endpoint for getting a list of users""" - auth_client.admin() + await auth_client.admin() # All users - response = auth_client.get("/users") - assert response.status_code == status.HTTP_200_OK - user_ids = [user["userId"] for user in response.json()['users']] - user_ids.remove(auth_client.user.user_id) - assert len(user_ids) == 2 - assert data["user1"] in user_ids - assert data["user2"] in user_ids + async with auth_client: + response = await auth_client.get("/users", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + user_ids = [user["userId"] for user in response.json()['users']] + user_ids.remove(auth_client.user.user_id) + assert len(user_ids) == 2 + assert data["user1"] in user_ids + assert data["user2"] in user_ids -def test_get_all_users_paginated(database_session: Session, auth_client: AuthClient): +async def test_get_all_users_paginated(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting a list of users""" for i in range(round(DB_PAGE_SIZE * 1.5)): database_session.add(models.User(name=f"User {i}", admin=False)) - database_session.commit() + await database_session.commit() - auth_client.admin() - response = auth_client.get("/users?page=0") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == DB_PAGE_SIZE - response = auth_client.get("/users?page=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + 1 - # +1 because Authclient.admin() also creates one user. + await auth_client.admin() + async with auth_client: + response = await auth_client.get("/users?page=0", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == DB_PAGE_SIZE + response = await auth_client.get("/users?page=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + 1 + # +1 because Authclient.admin() also creates one user. -def test_get_all_users_paginated_filter_name(database_session: Session, auth_client: AuthClient): +async def test_get_all_users_paginated_filter_name(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting a list of users with filter for name""" count = 0 for i in range(round(DB_PAGE_SIZE * 1.5)): database_session.add(models.User(name=f"User {i}", admin=False)) if "1" in str(i): count += 1 - database_session.commit() + await database_session.commit() - auth_client.admin() - response = auth_client.get("/users?page=0&name=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == min(DB_PAGE_SIZE, count) - response = auth_client.get("/users?page=1&name=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == max(count - round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE, 0) + await auth_client.admin() + async with auth_client: + response = await auth_client.get("/users?page=0&name=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == min(DB_PAGE_SIZE, count) + response = await auth_client.get("/users?page=1&name=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == max(count - round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE, 0) -def test_get_users_response(database_session: Session, auth_client: AuthClient, data: dict[str, str]): +async def test_get_users_response(database_session: AsyncSession, auth_client: AuthClient, data: dict[str, str]): """Test the response model of a user""" - auth_client.admin() - response = auth_client.get("/users") - users = response.json()["users"] - user1 = [user for user in users if user["userId"] == data["user1"]][0] - assert user1["auth"]["email"] == data["email1"] - assert user1["auth"]["authType"] == data["auth_type1"] - user2 = [user for user in users if user["userId"] == data["user2"]][0] - assert user2["auth"]["email"] == data["email2"] - assert user2["auth"]["authType"] == data["auth_type2"] - - -def test_get_all_admins(database_session: Session, auth_client: AuthClient, data: dict[str, str | int]): + await auth_client.admin() + async with auth_client: + response = await auth_client.get("/users", follow_redirects=True) + users = response.json()["users"] + user1 = [user for user in users if user["userId"] == data["user1"]][0] + assert user1["auth"]["email"] == data["email1"] + assert user1["auth"]["authType"] == data["auth_type1"] + user2 = [user for user in users if user["userId"] == data["user2"]][0] + assert user2["auth"]["email"] == data["email2"] + assert user2["auth"]["authType"] == data["auth_type2"] + + +async def test_get_all_admins(database_session: AsyncSession, auth_client: AuthClient, data: dict[str, str | int]): """Test endpoint for getting a list of admins""" - auth_client.admin() + await auth_client.admin() # All admins - response = auth_client.get("/users?admin=true") - assert response.status_code == status.HTTP_200_OK - user_ids = [user["userId"] for user in response.json()['users']] - user_ids.remove(auth_client.user.user_id) - assert [data["user1"]] == user_ids + async with auth_client: + response = await auth_client.get("/users?admin=true", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + user_ids = [user["userId"] for user in response.json()['users']] + user_ids.remove(auth_client.user.user_id) + assert [data["user1"]] == user_ids -def test_get_all_admins_paginated(database_session: Session, auth_client: AuthClient): +async def test_get_all_admins_paginated(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting a list of paginated admins""" count = 0 for i in range(round(DB_PAGE_SIZE * 3)): database_session.add(models.User(name=f"User {i}", admin=i % 2 == 0)) if i % 2 == 0: count += 1 - database_session.commit() + await database_session.commit() - auth_client.admin() - response = auth_client.get("/users?admin=true&page=0") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == min(count + 1, DB_PAGE_SIZE) - response = auth_client.get("/users?admin=true&page=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == min(count - DB_PAGE_SIZE + 1, DB_PAGE_SIZE + 1) - # +1 because Authclient.admin() also creates one user. + await auth_client.admin() + async with auth_client: + response = await auth_client.get("/users?admin=true&page=0", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == min(count + 1, DB_PAGE_SIZE) + response = await auth_client.get("/users?admin=true&page=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == min(count - DB_PAGE_SIZE + 1, DB_PAGE_SIZE + 1) + # +1 because Authclient.admin() also creates one user. -def test_get_all_non_admins_paginated(database_session: Session, auth_client: AuthClient): +async def test_get_all_non_admins_paginated(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting a list of paginated admins""" non_admins = [] for i in range(round(DB_PAGE_SIZE * 3)): user = models.User(name=f"User {i}", admin=i % 2 == 0) database_session.add(user) - database_session.commit() + await database_session.commit() if i % 2 != 0: non_admins.append(user.user_id) - database_session.commit() + await database_session.commit() - auth_client.admin() + await auth_client.admin() count = len(non_admins) - response = auth_client.get("/users?admin=false&page=0") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == min(count, DB_PAGE_SIZE) - for user in response.json()["users"]: - assert user["userId"] in non_admins - - response = auth_client.get("/users?admin=false&page=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == max(count - DB_PAGE_SIZE, 0) - - -def test_get_all_admins_paginated_filter_name(database_session: Session, auth_client: AuthClient): + async with auth_client: + response = await auth_client.get("/users?admin=false&page=0", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == min(count, DB_PAGE_SIZE) + for user in response.json()["users"]: + assert user["userId"] in non_admins + + response = await auth_client.get("/users?admin=false&page=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == max(count - DB_PAGE_SIZE, 0) + + +async def test_get_all_admins_paginated_filter_name(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting a list of paginated admins with filter for name""" for i in range(round(DB_PAGE_SIZE * 1.5)): database_session.add(models.User(name=f"User {i}", admin=True)) - database_session.commit() + await database_session.commit() - auth_client.admin() - response = auth_client.get("/users?admin=true&page=0") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == DB_PAGE_SIZE - response = auth_client.get("/users?admin=true&page=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + 1 + await auth_client.admin() + async with auth_client: + response = await auth_client.get("/users?admin=true&page=0", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == DB_PAGE_SIZE + response = await auth_client.get("/users?admin=true&page=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + 1 -def test_get_all_non_admins_paginated_filter_name(database_session: Session, auth_client: AuthClient): +async def test_get_all_non_admins_paginated_filter_name(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting a list of paginated admins""" non_admins = [] for i in range(round(DB_PAGE_SIZE * 3)): user = models.User(name=f"User {i}", admin=i % 2 == 0) database_session.add(user) - database_session.commit() + await database_session.commit() if i % 2 != 0 and "1" in str(i): non_admins.append(user.user_id) - database_session.commit() + await database_session.commit() - auth_client.admin() + await auth_client.admin() count = len(non_admins) - response = auth_client.get("/users?admin=false&page=0&name=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == min(count, DB_PAGE_SIZE) - for user in response.json()["users"]: - assert user["userId"] in non_admins + async with auth_client: + response = await auth_client.get("/users?admin=false&page=0&name=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == min(count, DB_PAGE_SIZE) + for user in response.json()["users"]: + assert user["userId"] in non_admins - response = auth_client.get("/users?admin=false&page=1&name=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == max(count - DB_PAGE_SIZE, 0) + response = await auth_client.get("/users?admin=false&page=1&name=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == max(count - DB_PAGE_SIZE, 0) -def test_get_users_from_edition(database_session: Session, auth_client: AuthClient, data: dict[str, str | int]): +async def test_get_users_from_edition(database_session: AsyncSession, auth_client: AuthClient, data: dict[str, str | int]): """Test endpoint for getting a list of users from a given edition""" - auth_client.admin() + await auth_client.admin() # All users from edition - response = auth_client.get(f"/users?edition={data['edition2']}") - assert response.status_code == status.HTTP_200_OK - user_ids = [user["userId"] for user in response.json()['users']] - assert [data["user2"]] == user_ids + async with auth_client: + response = await auth_client.get(f"/users?edition={data['edition2']}", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + user_ids = [user["userId"] for user in response.json()['users']] + assert [data["user2"]] == user_ids -def test_get_all_users_for_edition_paginated(database_session: Session, auth_client: AuthClient): +async def test_get_all_users_for_edition_paginated(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting a list of users""" edition = models.Edition(year=2022, name="ed2022") database_session.add(edition) for i in range(round(DB_PAGE_SIZE * 1.5)): database_session.add(models.User(name=f"User {i}", admin=False, editions=[edition])) - database_session.commit() + await database_session.commit() - auth_client.admin() - response = auth_client.get("/users?page=0&edition_name=ed2022") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == DB_PAGE_SIZE - response = auth_client.get("/users?page=1&edition_name=ed2022") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + 1 - # +1 because Authclient.admin() also creates one user. + await auth_client.admin() + async with auth_client: + response = await auth_client.get("/users?page=0&edition_name=ed2022", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == DB_PAGE_SIZE + response = await auth_client.get("/users?page=1&edition_name=ed2022", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + 1 + # +1 because Authclient.admin() also creates one user. -def test_get_all_users_for_edition_paginated_filter_user(database_session: Session, auth_client: AuthClient): +async def test_get_all_users_for_edition_paginated_filter_user(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting a list of users and filter on name""" edition = models.Edition(year=2022, name="ed2022") database_session.add(edition) @@ -247,72 +257,75 @@ def test_get_all_users_for_edition_paginated_filter_user(database_session: Sessi database_session.add(models.User(name=f"User {i}", admin=False, editions=[edition])) if "1" in str(i): count += 1 - database_session.commit() + await database_session.commit() - auth_client.admin() - response = auth_client.get("/users?page=0&edition_name=ed2022&name=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == min(count , DB_PAGE_SIZE) - response = auth_client.get("/users?page=1&edition_name=ed2022&name=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == max(count - DB_PAGE_SIZE, 0) + await auth_client.admin() + async with auth_client: + response = await auth_client.get("/users?page=0&edition_name=ed2022&name=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == min(count , DB_PAGE_SIZE) + response = await auth_client.get("/users?page=1&edition_name=ed2022&name=1", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == max(count - DB_PAGE_SIZE, 0) -def test_get_admins_from_edition(database_session: Session, auth_client: AuthClient, data: dict[str, str | int]): +async def test_get_admins_from_edition(database_session: AsyncSession, auth_client: AuthClient, data: dict[str, str | int]): """Test endpoint for getting a list of admins, edition should be ignored""" - auth_client.admin() + await auth_client.admin() # All admins from edition - response = auth_client.get(f"/users?admin=true&edition={data['edition1']}") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == 2 + async with auth_client: + response = await auth_client.get(f"/users?admin=true&edition={data['edition1']}", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == 2 - response = auth_client.get(f"/users?admin=true&edition={data['edition2']}") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['users']) == 2 + response = await auth_client.get(f"/users?admin=true&edition={data['edition2']}", follow_redirects=True) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['users']) == 2 -def test_get_all_users_excluded_edition_paginated(database_session: Session, auth_client: AuthClient): - auth_client.admin() +async def test_get_all_users_excluded_edition_paginated(database_session: AsyncSession, auth_client: AuthClient): + await auth_client.admin() edition_a = models.Edition(year=2022, name="edA") edition_b = models.Edition(year=2023, name="edB") database_session.add(edition_a) database_session.add(edition_b) - database_session.commit() + await database_session.commit() for i in range(round(DB_PAGE_SIZE * 1.5)): user_1 = models.User(name=f"User {i} - a", admin=False) user_2 = models.User(name=f"User {i} - b", admin=False) database_session.add(user_1) database_session.add(user_2) - database_session.commit() - database_session.execute(models.user_editions.insert(), [ + await database_session.commit() + await database_session.execute(models.user_editions.insert(), [ {"user_id": user_1.user_id, "edition_id": edition_a.edition_id}, {"user_id": user_2.user_id, "edition_id": edition_b.edition_id}, ]) - database_session.commit() - - a_users = auth_client.get(f"/users?page=0&exclude_edition=edB").json()["users"] - assert len(a_users) == DB_PAGE_SIZE - for user in a_users: - assert "b" not in user["name"] - assert len(auth_client.get(f"/users?page=1&exclude_edition=edB").json()["users"]) == \ - round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + 1 # auth_client is not a coach - - b_users = auth_client.get(f"/users?page=0&exclude_edition=edA").json()["users"] - assert len(b_users) == DB_PAGE_SIZE - for user in b_users: - assert "a" not in user["name"] - assert len(auth_client.get(f"/users?page=1&exclude_edition=edA").json()["users"]) == \ - round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + 1 # auth_client is not a coach - - -def test_get_all_users_excluded_edition_paginated_filter_name(database_session: Session, auth_client: AuthClient): - auth_client.admin() + await database_session.commit() + + async with auth_client: + a_users = (await auth_client.get(f"/users?page=0&exclude_edition=edB", follow_redirects=True)).json()["users"] + assert len(a_users) == DB_PAGE_SIZE + for user in a_users: + assert "b" not in user["name"] + assert len((await auth_client.get(f"/users?page=1&exclude_edition=edB", follow_redirects=True)).json()["users"]) == \ + round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + 1 # auth_client is not a coach + + b_users = (await auth_client.get(f"/users?page=0&exclude_edition=edA", follow_redirects=True)).json()["users"] + assert len(b_users) == DB_PAGE_SIZE + for user in b_users: + assert "a" not in user["name"] + assert len((await auth_client.get(f"/users?page=1&exclude_edition=edA", follow_redirects=True)).json()["users"]) == \ + round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + 1 # auth_client is not a coach + + +async def test_get_all_users_excluded_edition_paginated_filter_name(database_session: AsyncSession, auth_client: AuthClient): + await auth_client.admin() edition_a = models.Edition(year=2022, name="edA") edition_b = models.Edition(year=2023, name="edB") database_session.add(edition_a) database_session.add(edition_b) - database_session.commit() + await database_session.commit() count = 0 for i in range(round(DB_PAGE_SIZE * 1.5)): @@ -320,37 +333,38 @@ def test_get_all_users_excluded_edition_paginated_filter_name(database_session: user_2 = models.User(name=f"User {i} - b", admin=False) database_session.add(user_1) database_session.add(user_2) - database_session.commit() - database_session.execute(models.user_editions.insert(), [ + await database_session.commit() + await database_session.execute(models.user_editions.insert(), [ {"user_id": user_1.user_id, "edition_id": edition_a.edition_id}, {"user_id": user_2.user_id, "edition_id": edition_b.edition_id}, ]) if "1" in str(i): count += 1 - database_session.commit() - - a_users = auth_client.get(f"/users?page=0&exclude_edition=edB&name=1").json()["users"] - assert len(a_users) == min(count, DB_PAGE_SIZE) - for user in a_users: - assert "b" not in user["name"] - assert len(auth_client.get(f"/users?page=1&exclude_edition=edB&name=1").json()["users"]) == \ - max(count - DB_PAGE_SIZE, 0) - - b_users = auth_client.get(f"/users?page=0&exclude_edition=edA&name=1").json()["users"] - assert len(b_users) == min(count, DB_PAGE_SIZE) - for user in b_users: - assert "a" not in user["name"] - assert len(auth_client.get(f"/users?page=1&exclude_edition=edA&name=1").json()["users"]) == \ - max(count - DB_PAGE_SIZE, 0) - - -def test_get_all_users_for_edition_excluded_edition_paginated(database_session: Session, auth_client: AuthClient): - auth_client.admin() + await database_session.commit() + + async with auth_client: + a_users = (await auth_client.get(f"/users?page=0&exclude_edition=edB&name=1", follow_redirects=True)).json()["users"] + assert len(a_users) == min(count, DB_PAGE_SIZE) + for user in a_users: + assert "b" not in user["name"] + assert len((await auth_client.get(f"/users?page=1&exclude_edition=edB&name=1", follow_redirects=True)).json()["users"]) == \ + max(count - DB_PAGE_SIZE, 0) + + b_users = (await auth_client.get(f"/users?page=0&exclude_edition=edA&name=1", follow_redirects=True)).json()["users"] + assert len(b_users) == min(count, DB_PAGE_SIZE) + for user in b_users: + assert "a" not in user["name"] + assert len((await auth_client.get(f"/users?page=1&exclude_edition=edA&name=1", follow_redirects=True)).json()["users"]) == \ + max(count - DB_PAGE_SIZE, 0) + + +async def test_get_all_users_for_edition_excluded_edition_paginated(database_session: AsyncSession, auth_client: AuthClient): + await auth_client.admin() edition_a = models.Edition(year=2022, name="edA") edition_b = models.Edition(year=2023, name="edB") database_session.add(edition_a) database_session.add(edition_b) - database_session.commit() + await database_session.commit() correct_users_id = [] for i in range(round(DB_PAGE_SIZE * 1.5)): @@ -358,56 +372,58 @@ def test_get_all_users_for_edition_excluded_edition_paginated(database_session: user_2 = models.User(name=f"User {i} - b", admin=False) database_session.add(user_1) database_session.add(user_2) - database_session.commit() - database_session.execute(models.user_editions.insert(), [ + await database_session.commit() + await database_session.execute(models.user_editions.insert(), [ {"user_id": user_1.user_id, "edition_id": edition_a.edition_id}, {"user_id": user_2.user_id, "edition_id": edition_b.edition_id}, ]) if i % 2: - database_session.execute(models.user_editions.insert(), [ + await database_session.execute(models.user_editions.insert(), [ {"user_id": user_1.user_id, "edition_id": edition_b.edition_id}, ]) else: correct_users_id.append(user_1.user_id) - database_session.commit() - - users = auth_client.get(f"/users?page=0&exclude_edition=edB&edition=edA").json()["users"] - assert len(users) == len(correct_users_id) - for user in users: - assert user["userId"] in correct_users_id + await database_session.commit() + async with auth_client: + users = (await auth_client.get(f"/users?page=0&exclude_edition=edB&edition=edA", follow_redirects=True)).json()["users"] + assert len(users) == len(correct_users_id) + for user in users: + assert user["userId"] in correct_users_id -def test_get_users_invalid(database_session: Session, auth_client: AuthClient, data: dict[str, str | int]): +async def test_get_users_invalid(database_session: AsyncSession, auth_client: AuthClient, data: dict[str, str | int]): """Test endpoint for unvalid input""" - auth_client.admin() + await auth_client.admin() # Invalid input - response = auth_client.get("/users?admin=INVALID") - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + async with auth_client: + response = await auth_client.get("/users?admin=INVALID", follow_redirects=True) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY -def test_edit_admin_status(database_session: Session, auth_client: AuthClient): +async def test_edit_admin_status(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for editing the admin status of a user""" - auth_client.admin() + await auth_client.admin() # Create user user = models.User(name="user1", admin=False) database_session.add(user) - database_session.commit() + await database_session.commit() - response = auth_client.patch(f"/users/{user.user_id}", - data=dumps({"admin": True})) - assert response.status_code == status.HTTP_204_NO_CONTENT - assert user.admin + async with auth_client: + response = await auth_client.patch(f"/users/{user.user_id}", + json={"admin": True}) + assert response.status_code == status.HTTP_204_NO_CONTENT + assert user.admin - response = auth_client.patch(f"/users/{user.user_id}", - data=dumps({"admin": False})) - assert response.status_code == status.HTTP_204_NO_CONTENT - assert not user.admin + response = await auth_client.patch(f"/users/{user.user_id}", + json={"admin": False}) + assert response.status_code == status.HTTP_204_NO_CONTENT + assert not user.admin -def test_add_coach(database_session: Session, auth_client: AuthClient): +async def test_add_coach(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for adding coaches""" - auth_client.admin() + await auth_client.admin() # Create user user = models.User(name="user1", admin=False) database_session.add(user) @@ -416,19 +432,20 @@ def test_add_coach(database_session: Session, auth_client: AuthClient): edition = models.Edition(year=1, name="ed1") database_session.add(edition) - database_session.commit() + await database_session.commit() # Add coach - response = auth_client.post(f"/users/{user.user_id}/editions/{edition.name}") - assert response.status_code == status.HTTP_204_NO_CONTENT - coach = database_session.query(user_editions).one() - assert coach.user_id == user.user_id - assert coach.edition_id == edition.edition_id + async with auth_client: + response = await auth_client.post(f"/users/{user.user_id}/editions/{edition.name}") + assert response.status_code == status.HTTP_204_NO_CONTENT + coach = (await database_session.execute(select(user_editions))).one() + assert coach.user_id == user.user_id + assert coach.edition_id == edition.edition_id -def test_remove_coach(database_session: Session, auth_client: AuthClient): +async def test_remove_coach(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for removing coaches""" - auth_client.admin() + await auth_client.admin() # Create user user = models.User(name="user1") database_session.add(user) @@ -437,24 +454,25 @@ def test_remove_coach(database_session: Session, auth_client: AuthClient): edition = models.Edition(year=1, name="ed1") database_session.add(edition) - database_session.commit() + await database_session.commit() # Create request request = models.CoachRequest(user_id=user.user_id, edition_id=edition.edition_id) database_session.add(request) - database_session.commit() + await database_session.commit() # Remove coach - response = auth_client.delete(f"/users/{user.user_id}/editions/{edition.name}") - assert response.status_code == status.HTTP_204_NO_CONTENT - coach = database_session.query(user_editions).all() - assert len(coach) == 0 + async with auth_client: + response = await auth_client.delete(f"/users/{user.user_id}/editions/{edition.name}") + assert response.status_code == status.HTTP_204_NO_CONTENT + coach = (await database_session.execute(select(user_editions))).scalars().all() + assert len(coach) == 0 -def test_remove_coach_all_editions(database_session: Session, auth_client: AuthClient): +async def test_remove_coach_all_editions(database_session: AsyncSession, auth_client: AuthClient): """Test removing a user as coach from all editions""" - auth_client.admin() + await auth_client.admin() # Create user user1 = models.User(name="user1", admin=False) @@ -470,25 +488,26 @@ def test_remove_coach_all_editions(database_session: Session, auth_client: AuthC database_session.add(edition2) database_session.add(edition3) - database_session.commit() + await database_session.commit() # Create coach role - database_session.execute(models.user_editions.insert(), [ + await database_session.execute(models.user_editions.insert(), [ {"user_id": user1.user_id, "edition_id": edition1.edition_id}, {"user_id": user1.user_id, "edition_id": edition2.edition_id}, {"user_id": user1.user_id, "edition_id": edition3.edition_id}, {"user_id": user2.user_id, "edition_id": edition2.edition_id}, ]) - response = auth_client.delete(f"/users/{user1.user_id}/editions") - assert response.status_code == status.HTTP_204_NO_CONTENT - coach = database_session.query(user_editions).all() - assert len(coach) == 1 + async with auth_client: + response = await auth_client.delete(f"/users/{user1.user_id}/editions") + assert response.status_code == status.HTTP_204_NO_CONTENT + coach = (await database_session.execute(select(user_editions))).all() + assert len(coach) == 1 -def test_get_all_requests(database_session: Session, auth_client: AuthClient): +async def test_get_all_requests(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting all userrequests""" - auth_client.admin() + await auth_client.admin() # Create user user1 = models.User(name="user1") @@ -502,7 +521,7 @@ def test_get_all_requests(database_session: Session, auth_client: AuthClient): database_session.add(edition1) database_session.add(edition2) - database_session.commit() + await database_session.commit() # Create request request1 = models.CoachRequest(user_id=user1.user_id, edition_id=edition1.edition_id) @@ -510,17 +529,18 @@ def test_get_all_requests(database_session: Session, auth_client: AuthClient): database_session.add(request1) database_session.add(request2) - database_session.commit() + await database_session.commit() - response = auth_client.get("/users/requests") - assert response.status_code == status.HTTP_200_OK - user_ids = [request["user"]["userId"] for request in response.json()['requests']] - assert len(user_ids) == 2 - assert user1.user_id in user_ids - assert user2.user_id in user_ids + async with auth_client: + response = await auth_client.get("/users/requests") + assert response.status_code == status.HTTP_200_OK + user_ids = [request["user"]["userId"] for request in response.json()['requests']] + assert len(user_ids) == 2 + assert user1.user_id in user_ids + assert user2.user_id in user_ids -def test_get_all_requests_paginated(database_session: Session, auth_client: AuthClient): +async def test_get_all_requests_paginated(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting a paginated list of requests""" edition = models.Edition(year=2022, name="ed2022") @@ -528,18 +548,19 @@ def test_get_all_requests_paginated(database_session: Session, auth_client: Auth user = models.User(name=f"User {i}", admin=False) database_session.add(user) database_session.add(models.CoachRequest(user=user, edition=edition)) - database_session.commit() + await database_session.commit() - auth_client.admin() - response = auth_client.get("/users/requests?page=0") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['requests']) == DB_PAGE_SIZE - response = auth_client.get("/users/requests?page=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['requests']) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + await auth_client.admin() + async with auth_client: + response = await auth_client.get("/users/requests?page=0") + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['requests']) == DB_PAGE_SIZE + response = await auth_client.get("/users/requests?page=1") + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['requests']) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE -def test_get_all_requests_paginated_filter_name(database_session: Session, auth_client: AuthClient): +async def test_get_all_requests_paginated_filter_name(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting a paginated list of requests""" edition = models.Edition(year=2022, name="ed2022") @@ -550,20 +571,21 @@ def test_get_all_requests_paginated_filter_name(database_session: Session, auth_ database_session.add(models.CoachRequest(user=user, edition=edition)) if "1" in str(i): count += 1 - database_session.commit() + await database_session.commit() - auth_client.admin() - response = auth_client.get("/users/requests?page=0&user=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['requests']) == min(DB_PAGE_SIZE, count) - response = auth_client.get("/users/requests?page=1&user=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['requests']) == max(count-DB_PAGE_SIZE, 0) + await auth_client.admin() + async with auth_client: + response = await auth_client.get("/users/requests?page=0&user=1") + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['requests']) == min(DB_PAGE_SIZE, count) + response = await auth_client.get("/users/requests?page=1&user=1") + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['requests']) == max(count-DB_PAGE_SIZE, 0) -def test_get_all_requests_from_edition(database_session: Session, auth_client: AuthClient): +async def test_get_all_requests_from_edition(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting all userrequests of a given edition""" - auth_client.admin() + await auth_client.admin() # Create user user1 = models.User(name="user1") @@ -577,7 +599,7 @@ def test_get_all_requests_from_edition(database_session: Session, auth_client: A database_session.add(edition1) database_session.add(edition2) - database_session.commit() + await database_session.commit() # Create request request1 = models.CoachRequest(user_id=user1.user_id, edition_id=edition1.edition_id) @@ -585,22 +607,23 @@ def test_get_all_requests_from_edition(database_session: Session, auth_client: A database_session.add(request1) database_session.add(request2) - database_session.commit() + await database_session.commit() - response = auth_client.get(f"/users/requests?edition={edition1.name}") - assert response.status_code == status.HTTP_200_OK - requests = response.json()['requests'] - assert len(requests) == 1 - assert user1.user_id == requests[0]["user"]["userId"] + async with auth_client: + response = await auth_client.get(f"/users/requests?edition={edition1.name}") + assert response.status_code == status.HTTP_200_OK + requests = response.json()['requests'] + assert len(requests) == 1 + assert user1.user_id == requests[0]["user"]["userId"] - response = auth_client.get(f"/users/requests?edition={edition2.name}") - assert response.status_code == status.HTTP_200_OK - requests = response.json()['requests'] - assert len(requests) == 1 - assert user2.user_id == requests[0]["user"]["userId"] + response = await auth_client.get(f"/users/requests?edition={edition2.name}") + assert response.status_code == status.HTTP_200_OK + requests = response.json()['requests'] + assert len(requests) == 1 + assert user2.user_id == requests[0]["user"]["userId"] -def test_get_all_requests_for_edition_paginated(database_session: Session, auth_client: AuthClient): +async def test_get_all_requests_for_edition_paginated(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting a paginated list of requests""" edition = models.Edition(year=2022, name="ed2022") @@ -608,18 +631,19 @@ def test_get_all_requests_for_edition_paginated(database_session: Session, auth_ user = models.User(name=f"User {i}", admin=False) database_session.add(user) database_session.add(models.CoachRequest(user=user, edition=edition)) - database_session.commit() + await database_session.commit() - auth_client.admin() - response = auth_client.get("/users/requests?page=0&edition_name=ed2022") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['requests']) == DB_PAGE_SIZE - response = auth_client.get("/users/requests?page=1&edition_name=ed2022") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['requests']) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + await auth_client.admin() + async with auth_client: + response = await auth_client.get("/users/requests?page=0&edition_name=ed2022") + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['requests']) == DB_PAGE_SIZE + response = await auth_client.get("/users/requests?page=1&edition_name=ed2022") + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['requests']) == round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE -def test_get_all_requests_for_edition_paginated_filter_name(database_session: Session, auth_client: AuthClient): +async def test_get_all_requests_for_edition_paginated_filter_name(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for getting a paginated list of requests""" edition = models.Edition(year=2022, name="ed2022") @@ -630,20 +654,21 @@ def test_get_all_requests_for_edition_paginated_filter_name(database_session: Se database_session.add(models.CoachRequest(user=user, edition=edition)) if "1" in str(i): count += 1 - database_session.commit() + await database_session.commit() - auth_client.admin() - response = auth_client.get("/users/requests?page=0&edition_name=ed2022&user=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['requests']) == min(DB_PAGE_SIZE, count) - response = auth_client.get("/users/requests?page=1&edition_name=ed2022&user=1") - assert response.status_code == status.HTTP_200_OK - assert len(response.json()['requests']) == max(count-DB_PAGE_SIZE, 0) + await auth_client.admin() + async with auth_client: + response = await auth_client.get("/users/requests?page=0&edition_name=ed2022&user=1") + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['requests']) == min(DB_PAGE_SIZE, count) + response = await auth_client.get("/users/requests?page=1&edition_name=ed2022&user=1") + assert response.status_code == status.HTTP_200_OK + assert len(response.json()['requests']) == max(count-DB_PAGE_SIZE, 0) -def test_accept_request(database_session, auth_client: AuthClient): +async def test_accept_request(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for accepting a coach request""" - auth_client.admin() + await auth_client.admin() # Create user user1 = models.User(name="user1") database_session.add(user1) @@ -652,24 +677,25 @@ def test_accept_request(database_session, auth_client: AuthClient): edition1 = models.Edition(year=1, name="ed1") database_session.add(edition1) - database_session.commit() + await database_session.commit() # Create request request1 = models.CoachRequest(user_id=user1.user_id, edition_id=edition1.edition_id) database_session.add(request1) - database_session.commit() + await database_session.commit() - response = auth_client.post(f"users/requests/{request1.request_id}/accept") - assert response.status_code == status.HTTP_204_NO_CONTENT + async with auth_client: + response = await auth_client.post(f"users/requests/{request1.request_id}/accept") + assert response.status_code == status.HTTP_204_NO_CONTENT - assert len(user1.editions) == 1 - assert user1.editions[0].edition_id == edition1.edition_id + assert len(user1.editions) == 1 + assert user1.editions[0].edition_id == edition1.edition_id -def test_reject_request(database_session, auth_client: AuthClient): +async def test_reject_request(database_session: AsyncSession, auth_client: AuthClient): """Test endpoint for rejecting a coach request""" - auth_client.admin() + await auth_client.admin() # Create user user1 = models.User(name="user1") database_session.add(user1) @@ -678,19 +704,46 @@ def test_reject_request(database_session, auth_client: AuthClient): edition1 = models.Edition(year=1, name="ed1") database_session.add(edition1) - database_session.commit() + await database_session.commit() # Create request request1 = models.CoachRequest(user_id=user1.user_id, edition_id=edition1.edition_id) database_session.add(request1) - database_session.commit() + await database_session.commit() - response = auth_client.post(f"users/requests/{request1.request_id}/reject") - assert response.status_code == status.HTTP_204_NO_CONTENT + async with auth_client: + response = await auth_client.post(f"users/requests/{request1.request_id}/reject") + assert response.status_code == status.HTTP_204_NO_CONTENT - requests = database_session.query(CoachRequest).all() - assert len(requests) == 0 + requests = (await database_session.execute(select(CoachRequest))).scalars().all() + assert len(requests) == 0 - response = auth_client.post("users/requests/INVALID/reject") - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + response = await auth_client.post("users/requests/INVALID/reject") + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + +async def test_get_current_user(database_session: AsyncSession, auth_client: AuthClient): + """Test getting the current user from their access token""" + edition = Edition(year=2022, name="ed2022") + user = User(name="Pytest Admin", admin=True, editions=[edition]) + database_session.add(edition) + database_session.add(user) + await database_session.commit() + auth_client.login(user) + + async with auth_client: + response = await auth_client.get("/users/current") + assert response.status_code == status.HTTP_200_OK + assert response.json()["userId"] == auth_client.user.user_id + assert len(response.json()["editions"]) == 1 + assert response.json()["editions"][0]["name"] == edition.name + + +async def test_current_user(database_session: AsyncSession, auth_client: AuthClient): + """test current user""" + await auth_client.admin() + async with auth_client: + response = await auth_client.get("users/current") + assert response.status_code == status.HTTP_200_OK + assert response.json() == {'userId': 1, 'name': 'Pytest Admin', 'admin': True, 'auth': None, 'editions': []} diff --git a/backend/tests/test_schemas/test_validators.py b/backend/tests/test_schemas/test_validators.py index 770d621e0..d68d03a0c 100644 --- a/backend/tests/test_schemas/test_validators.py +++ b/backend/tests/test_schemas/test_validators.py @@ -1,7 +1,7 @@ import pytest from src.app.exceptions.validation_exception import ValidationException -from src.app.schemas.validators import validate_email_format +from src.app.schemas.validators import validate_email_format, validate_edition, validate_url def test_email_address(): @@ -22,3 +22,35 @@ def test_email_address(): validate_email_format("A@Couple@Of@@s") validate_email_format("test.some@thi.ng") + + +def test_edition_name(): + """Test the validation of edition names""" + with pytest.raises(ValidationException): + validate_edition("New Edition") + + with pytest.raises(ValidationException): + validate_edition("NewEdition?!") + + with pytest.raises(ValidationException): + validate_edition("(NewEdition)") + + validate_edition("Edition2022") + + validate_edition("Edition_2022") + + validate_edition("edition2022") + + validate_edition("Edition-2022") + + +def test_validate_url(): + """Test the validation of an url""" + validate_url("https://info.com") + validate_url("http://info") + with pytest.raises(ValidationException): + validate_url("ssh://info.com") + with pytest.raises(ValidationException): + validate_url("http:/info.com") + with pytest.raises(ValidationException): + validate_url("https:/info.com") diff --git a/backend/tests/test_utils/test_dependencies.py b/backend/tests/test_utils/test_dependencies.py new file mode 100644 index 000000000..4c1ef2437 --- /dev/null +++ b/backend/tests/test_utils/test_dependencies.py @@ -0,0 +1,49 @@ +import pytest +from sqlalchemy.ext.asyncio import AsyncSession +from src.app.exceptions.authentication import MissingPermissionsException +from src.app.exceptions.util import NotFound +from src.app.utils.dependencies import get_project_role, require_auth, require_coach_ws, get_project +from src.database.models import Edition, User, Project, ProjectRole, Skill + + +async def test_require_coach_ws(database_session: AsyncSession): + """test require coach websockets""" + edition: Edition = Edition(year=2022, name="ed22") + database_session.add(edition) + coach1: User = User(name="coach1", editions=[edition]) + coach2: User = User(name="coach2", editions=[]) + database_session.add(coach1) + database_session.add(coach2) + await database_session.commit() + coach_ws: User = await require_coach_ws(edition=edition, user=coach1) + assert coach_ws == coach1 + with pytest.raises(MissingPermissionsException): + await require_auth(coach2) + + +async def test_project_wrong_edition(database_session: AsyncSession): + """test project wrong edition""" + edition1: Edition = Edition(year=2022, name="ed22") + edition2: Edition = Edition(year=2023, name="ed23") + database_session.add(edition1) + database_session.add(edition2) + project: Project = Project(name="Project", edition=edition1) + database_session.add(project) + await database_session.commit() + with pytest.raises(NotFound): + await get_project(1, database_session, edition2) + + +async def test_project_role_wrong_project(database_session: AsyncSession): + """test project role wrong project""" + edition1: Edition = Edition(year=2022, name="ed22") + database_session.add(edition1) + project1: Project = Project(name="Project1", + edition=edition1, + project_roles=[ProjectRole(slots=1, skill=Skill(name="skill"))]) + project2: Project = Project(name="Project2", edition=edition1) + database_session.add(project1) + database_session.add(project2) + await database_session.commit() + with pytest.raises(NotFound): + await get_project_role(1, project2, database_session) diff --git a/backend/tests/test_utils/test_websockets.py b/backend/tests/test_utils/test_websockets.py new file mode 100644 index 000000000..e88c660b2 --- /dev/null +++ b/backend/tests/test_utils/test_websockets.py @@ -0,0 +1,97 @@ +from asyncio import Queue + +import pytest + +from src.app.utils.websockets import LiveEventParameters, EventType, DataPublisher + + +async def test_parse_event_type_project(): + live_event: LiveEventParameters = LiveEventParameters( + 'POST', + {'project_id': 1} + ) + + assert live_event.event_type == EventType.PROJECT + + +async def test_parse_event_type_project_role(): + live_event: LiveEventParameters = LiveEventParameters( + 'POST', + {'project_id': 1, 'project_role_id': 2} + ) + + assert live_event.event_type == EventType.PROJECT_ROLE + + +async def test_parse_event_type_pr_suggestion(): + live_event: LiveEventParameters = LiveEventParameters( + 'POST', + {'project_id': 1, 'project_role_id': 2, 'student_id': 2} + ) + + assert live_event.event_type == EventType.PROJECT_ROLE_SUGGESTION + + +async def test_parse_event_type_student(): + live_event: LiveEventParameters = LiveEventParameters( + 'POST', + {'student_id': 2} + ) + + assert live_event.event_type == EventType.STUDENT + + +async def test_parse_event_type_student_suggestion(): + live_event: LiveEventParameters = LiveEventParameters( + 'POST', + {'student_id': 2, 'suggestion_id': 1} + ) + + assert live_event.event_type == EventType.STUDENT_SUGGESTION + + +async def test_parse_event_type_invalid(): + with pytest.raises(Exception): + LiveEventParameters( + 'POST', + {'blargh': 1} + ) + + +async def test_event_format(): + live_event: dict = await LiveEventParameters( + 'POST', + {'student_id': 2, 'suggestion_id': 1} + ).json() + + assert 'method' in live_event + assert 'pathIds' in live_event + assert type(live_event['pathIds']) == dict + assert 'eventType' in live_event + + +async def test_data_publisher_subscribe(): + dp: DataPublisher = DataPublisher() + assert await dp.subscribe() is not None + assert len(dp.queues) == 1 + + +async def test_data_publisher_unsubscribe(): + dp: DataPublisher = DataPublisher() + q: Queue = await dp.subscribe() + await dp.unsubscribe(q) + assert len(dp.queues) == 0 + + +async def test_data_publisher_broadcast(): + dp: DataPublisher = DataPublisher() + qs: list[Queue] = [await dp.subscribe() for _ in range(10)] + live_event: LiveEventParameters = LiveEventParameters( + 'POST', + {'project_id': 1} + ) + + await dp.broadcast(live_event) + for q in qs: + data: dict = await q.get() + assert data == await live_event.json() diff --git a/backend/tests/utils/authorization/auth_client.py b/backend/tests/utils/authorization/auth_client.py index 900c996b6..458ade66d 100644 --- a/backend/tests/utils/authorization/auth_client.py +++ b/backend/tests/utils/authorization/auth_client.py @@ -2,20 +2,20 @@ from typing import Text from requests import Response -from sqlalchemy.orm import Session -from starlette.testclient import TestClient +from sqlalchemy.ext.asyncio import AsyncSession +from httpx import AsyncClient from src.app.logic.security import create_tokens from src.database.models import User, Edition -class AuthClient(TestClient): +class AuthClient(AsyncClient): """Custom TestClient that handles authentication to make tests more compact""" user: User | None = None headers: dict[str, str] | None = None - session: Session + session: AsyncSession - def __init__(self, session: Session, *args, **kwargs): + def __init__(self, session: AsyncSession, *args, **kwargs): super().__init__(*args, **kwargs) self.session = session @@ -26,16 +26,16 @@ def invalid(self): "Authorization": "Bearer If I can't scuba, then what has this all been about? What am I working towards?" } - def admin(self): + async def admin(self): """Sign in as an admin for all future requests""" # Create a new user in the db admin = User(name="Pytest Admin", admin=True) self.session.add(admin) - self.session.commit() + await self.session.commit() self.login(admin) - def coach(self, edition: Edition): + async def coach(self, edition: Edition): """Sign in as a coach for all future requests Assigns the coach to the edition """ @@ -45,7 +45,7 @@ def coach(self, edition: Edition): # Link the coach to the edition coach.editions.append(edition) self.session.add(coach) - self.session.commit() + await self.session.commit() self.login(coach) @@ -59,32 +59,32 @@ def login(self, user: User): # Add auth headers into dict self.headers = {"Authorization": f"Bearer {access_token}"} - def delete(self, url: Text | None, **kwargs) -> Response: + async def delete(self, url: Text | None, **kwargs) -> Response: if self.headers is not None: kwargs["headers"] = self.headers - return super().delete(url, **kwargs) + return await super().delete(url, **kwargs) - def get(self, url: Text | None, **kwargs) -> Response: + async def get(self, url: Text | None, **kwargs) -> Response: if self.headers is not None: kwargs["headers"] = self.headers - return super().get(url, **kwargs) + return await super().get(url, **kwargs) - def patch(self, url: Text | None, **kwargs) -> Response: + async def patch(self, url: Text | None, **kwargs) -> Response: if self.headers is not None: kwargs["headers"] = self.headers - return super().patch(url, **kwargs) + return await super().patch(url, **kwargs) - def post(self, url: Text | None, **kwargs) -> Response: + async def post(self, url: Text | None, **kwargs) -> Response: if self.headers is not None: kwargs["headers"] = self.headers - return super().post(url, **kwargs) + return await super().post(url, **kwargs) - def put(self, url: Text | None, **kwargs) -> Response: + async def put(self, url: Text | None, **kwargs) -> Response: if self.headers is not None: kwargs["headers"] = self.headers - return super().put(url, **kwargs) + return await super().put(url, **kwargs) diff --git a/codecov.yaml b/codecov.yaml new file mode 100644 index 000000000..6fd5027ca --- /dev/null +++ b/codecov.yaml @@ -0,0 +1,13 @@ +comment: + layout: "reach, diff, flags, files" + behavior: default + require_changes: false # if true: only post the comment if coverage changes + require_base: no # [yes :: must have a base report to post] + require_head: yes # [yes :: must have a head report to post] + +coverage: + round: down + precision: 5 + +ignore: + - "./tests/*" diff --git a/files/Assignment.md b/files/Assignment.md index ef7c9f758..3188dcc96 100644 --- a/files/Assignment.md +++ b/files/Assignment.md @@ -8,7 +8,7 @@ Import data from selection form in the tool via the webhook - Receive invitation link -- Sign up via mail account, Google or GitHub +- Sign up via mail account or GitHub - Admin approves @@ -16,11 +16,11 @@ Import data from selection form in the tool via the webhook - Restricted access for coaches - - Can’t see “manage users” + - Can’t see “manage users” - - Can’t confirm student selection, only suggest - - - Can't see multiple editions + - Can’t confirm student selection, only suggest + + - Can't see multiple editions - Partners do not have access, and are not involved in selection @@ -36,17 +36,17 @@ Import data from selection form in the tool via the webhook - Filter & search: - - By name + - By name - - By skill + - By skill - - Alumni + - Alumni - - Student coach volunteer + - Student coach volunteer - - Decided on “yes, maybe, no” or undecided + - Decided on “yes, maybe, no” or undecided - - Reset (remove all filters) + - Reset (remove all filters) ### BONUS: diff --git a/files/user_manual.md b/files/user_manual.md index 5baf11504..558985c5b 100644 --- a/files/user_manual.md +++ b/files/user_manual.md @@ -2,6 +2,7 @@ In this file, we describe how the selection tool is meant to be used by the end users. This is divided in the different parts of the website. + ## Logging in After you have registered yourself and have been approved by one of the administrators of the selection tool, you can log in to the website. @@ -17,15 +18,17 @@ There are different ways to log in, depending on the way in which you have regis 1. Click the "Log in" button with the GitHub logo. -### Google -1. Click the "Log in" button with the Google logo. +## Logging out + +To log out, click on the **Log Out** button in the top-right corner of the page. -## Admins -This section is for admins. It contains all features to manage users. A user is someone who uses the tool (this does not include students). A user can be coach of one or more editions. He can only see data and work (making suggestions...) on these editions. A user can be admin of the tool. An admin can see/edit/delete all data from all editions and manage other users. This role is only for fully trusted people. An admin doesn't need to be coach from an edition to participate in the selection process. +## Managing Users (Admin-only) -The management is split into two pages. The first one is to manage coaches of the currently selected edition. The other is to manage admins. Both pages can be found in the **Users** tab in the navigation bar. +This section is for admins. It contains all features to manage users. A user is someone who uses the tool (this does not include students). A user can be coach of one or more editions. He can only see data and work (making suggestions...) on these editions. A user can be admin of the tool. An admin can see/edit/delete all data from all editions and manage other users. This role is only for fully trusted people. An admin doesn't need to be coach in an edition to participate in the selection process. + +The management is split into two pages. The first one is to manage coaches of the currently selected edition. The other is to manage admins (these are not linked to a specific edition). Both pages can be found in the **Users** tab in the navigation bar. ### Coaches @@ -42,20 +45,20 @@ At the top left, you can invite someone via an invite link. You can choose betwe At the top middle of the page, you find a dropdown labeled **Requests**. When you expand the dropdown, you can see a list of all pending user requests. These are all users who used an invite link to create an account, and haven't been accepted (or declined) yet. -Note: the list only contains requests from the current selected edition. Each edition has its own requests. +**Note:** the list only contains requests from the current selected edition. Each edition has its own requests. -The list can be filtered by name. Each row of the table contains the name and email address of a person. The email contains an icon indicating whether the person registered via email, GitHub or Google. Next to each row there are two buttons to accept or reject the person. When a person is accepted, he will automatically be added as coach to the current edition. +The list can be filtered by name. Each row of the table contains the name and email address of a person. The email contains an icon indicating whether the person registered via email or GitHub. Next to each row there are two buttons to accept or reject the person. When a person is accepted, he will automatically be added as coach to the current edition. #### Coaches -A the centre of the page, you can find a list of all users who are coach in the current edition. As in the Requests list, each row contains the name and email address of a user. The list can be filtered by name. +At the centre of the page, you can find a list of all users who are coach in the current edition. As in the Requests list, each row contains the name and email address of a user. The list can be filtered by name. Next to the email address, there is a button to remove the user as coach from the currently selected edition. Once clicked, you get two choices: -- **Remove from all editions**: The user will be removed as coach from all editions. If the user is not an admin, he won't be able to see any data from any edition anymore -- **Remove from {Edition name}**: The user will be removed as coach from the current selected edition. He will still be able to see data from any other edition wherefrom he is coach. +- **Remove from all editions**: The user will be removed as coach from all editions. If the user is not an admin, he won't be able to see any data from any edition anymore. +- **Remove from {Edition name}**: The user will be removed as coach from the current selected edition. He will still be able to see data from all other editions in which they are coach. -At the top right of the list, there is a button to add a user as coach to the selected edition. This can be used if a user of a previous edition needs to be a coach in the current edition. You can only add existing users via this button. Once clicked, you see a prompt to search for a user's name. After typing the name of the user, a list of users whose name contains the typed text will be shown. You can select the desired user, check if the email and register-method are correct and add him as coach to the current edition. A user who is added as coach will be able to see all data of the current edition and participate in the selection process. +At the top-right of the list, there is a button to add a user as coach to the selected edition. This can be used if a user of a previous edition needs to be a coach in the current edition. You can only add existing users via this button. Once clicked, you see a prompt to search for a user's name. After typing the name of the user, a list of users whose names contain the typed text will be shown. You can select the desired user, check if the email and register-method are correct and add him as coach to the current edition. A user who is added as coach will be able to see all data of the current edition and participate in the selection process. ### Admins @@ -63,13 +66,14 @@ This page consists of a list of all users who are admin. An admin can see all ed Next to the email address, there is a button to remove a user as admin. Once clicked, you get two choices: -- **Remove admin**: Remove the given user as admin. He will stay coach for editions whereto he was assigned -- **Remove as admin and coach**: Remove the given user as admin and remove him as coach from every edition. The user won't be able to see any data from any edition. +- **Remove admin**: Remove the given user as admin. They will stay coach for editions they were assigned to. +- **Remove as admin and coach**: Remove the given user as admin and remove them as coach from every edition. The user won't be able to see any data from any editions. -At the top right of the list, there is a button to add a user as admin. Once clicked, you see a prompt to search for a user's name. After typing the name of the user, a list of users whose names contain the typed text will be shown. You can select the desired user, check if the email and register-method are correct and add him as admin. +At the top-right of the list, there is a button to add a user as admin. Once clicked, you see a prompt to search for a user's name. After typing the name of the user, a list of users whose names contain the typed text will be shown. You can select the desired user, check if the email and register-method are correct and add him as admin. **Warning**: A user who is added as admin will be able to edit and delete all data. He will be able to add and remove other admins. + ## Editions This section contains all actions related to managing your editions. @@ -87,9 +91,9 @@ This page lists all editions, and contains buttons for: In order to create new editions, head over to the Editions Page (see [Viewing a list of available editions](#viewing-a-list-of-available-editions)). In the top-right of the page, you should see a "+ Create Edition"-button. -- Click the "+ Create Edition"-button -- Fill in the fields in the form presented to you -- Click the "Submit"-button +- Click the "+ Create Edition"-button. +- Fill in the fields in the form presented to you (only alphanumerical characters, dashes and underscores are allowed as the name for an edition). +- Click the "Submit"-button. You've now created a new edition to which you can add coaches, projects, and students. @@ -97,8 +101,8 @@ You've now created a new edition to which you can add coaches, projects, and stu In order to delete editions, head over to the Editions Page (see [Viewing a list of available editions](#viewing-a-list-of-available-editions)). Every entry in the list will have a "Delete Edition" on the right. -- Click the "Delete Edition"-button of the edition you want to delete -- Follow the on-screen instructions +- Click the "Delete Edition"-button of the edition you want to delete. +- Follow the on-screen instructions. **Warning**: Deleting an edition is a **very dangerous operation** and **can not be undone**. As none of the linked data can be viewed without an edition, this means that deleting an edition _also_ deletes: @@ -111,11 +115,15 @@ We have made a component to quickly go to a page from a previous edition. In the _**Note**: This dropdown is hidden if you cannot see any editions, as it would be empty. If you are an admin, create a new edition. If you are a coach, wait for an admin to add you to an edition._ -- Click the dropdown in the navbar to open it -- In the dropdown, click on the edition you'd like to switch to +- Click the dropdown in the navbar to open it. +- In the dropdown, click on the edition you'd like to switch to. You have now set another edition as your "current edition". This means that navigating through the navbar will show results for that specific edition. +### Making an edition read-only + +Old editions can be made read-and-delete-only by clicking on the 'editable' label on the Editions page. This can be undone by clicking the label again. + ## Projects This section contains all actions related to managing projects. @@ -124,14 +132,109 @@ This section contains all actions related to managing projects. You can navigate to the "Projects page" by clicking the **Projects** button. Here you can see all the projects that belong to the current edition. In the short overview of a project you can see the partners, coaches and the number of students needed for this project. -You can also filter on project name and on the projects where you are a coach of. To filter on name enter a name in the search field and press enter or click the **Search** button. To filter on your own projects toggle the **Only own projects** switch. +You can also filter on project name and on the projects which you are a coach of. To filter on name enter a name in the search field and press enter or click the **Search** button. To filter on your own projects toggle the **Only own projects** switch. + +### Deleting a project (Admin-only) -To get more search results click the **Load more projects** button located at the bottom of the search results +To delete a project click the **trash** button located on the top-right of a project card or next to the project name in the detailed project page. A pop-up will appear to confirm your decision. Press **Delete** again to delete the project or cancel the delete operation. -### Detailed view of a project +### Conflicts + +As coaches can suggest a student for multiple projects, but a student can only work on one, conflicts can happen. To definitively confirm a student for a project, all conflicts involving that student will first need to be resolved. + +To see an overview of all current conflicts, click on the red button with the warning sign at the top right of the "Projects page". + + +## Detailed view of a project To get more in depth with a project click the title of the project. This will take you to the "Project page" of that specific project. -### Delete a project +### Editing a project (Admin-only) + +To edit a project, click on the pencil icon next to the project's name, now you can change all the attributes of the project. + +### Suggesting a student for a project + +To suggest a student for a project, drag the student from the list on the left-hand side to the skill you want to suggest the student for. A pop-up will appear asking for a motivation for the suggestion. + +### Removing a student from a project (Admin-only) + +To remove a student from a project, click on the red trash icon on the top-right of the student's card on the "Project page". + +### Adding a skill to a project (Admin-only) + +A skill contains a number of slots where students can be placed in to fulfill that skill for that project. + +To add a new skill requirement to a project, click on the **Add new skill** button on the bottom of the "Project page". + +### Removing a skill from a project (Admin-only) + +To remove a skill from a project, click on the red trash icon on the top right of the card of the skill on the "Project page". + + +## State history of a student (Admin-only) + +A student can be at different stages throughout the selection process, at some stages, an email needs to be sent to the student to inform them of their state change (e.g. the student has been accepted/denied). + +To view a student's state history (i.e. all states the student has ever been in), navigate to the page with the student's details, and click on the "See State History" button. + +The state history will be shown in a table, with the most recent state at the top. + + +## Overview of states (Admin-only) + +To see the overview of states, click on "Students" in the navbar on top of the page, and then click on "State Overview". + +The overview of the states is a table of all students of an edition, together with the most recent state that has been assigned to them. + +This table needs to be manually maintained (i.e. when a new decision has been made about a student, someone has to update this list). + +### Searching and Filtering + +The overview table allows you to search for a particular student or filter based on one or more states. + +To search for a student: type (the beginning of) a student's name in the search bar. + +To filter based on email states: select one or more states from the list. + +These two things (searching and filtering) can also be combined. + +### Updating the state overview + +When a new state is assigned to one or more students, the list needs to be updated. + +This can be done by selecting these students in the table, clicking on the "Set state of selected students", and choosing the new state from the dropdown. + + +## Students + +### Viewing a student + +To view the details of a student, click on the student's name in the list on the left-hand side. + +### Making a suggestion for a student + +Every coach can make a suggestion for a student, where they express whether they find the student interesting to use in their project or not. +To do this, go to the student's detail page and click on the appropriate suggestion you want to make for that student. A popup will appear where you can enter an argumentation for this decision. + +### Making a definitive decision for a student (Admin-only) + +An admin can make a definitive decision for a student, by taking into account the multiple suggestions from the coaches. This is where an admin decides if a student is accepted into the programme or not. +To make a definitive decision, go to the student's detail page and click on the **Confirm** button. A popup will appear where you can select the decision you want to make. + +### Removing a student (Admin-only) + +To remove a student, click on the red trash icon on the bottom of the student's page. + +### Searching and filtering in the student's list + +The student's list on the left-hand side can be searched through (by typing the name of the student in the search bar) and can also be filtered by multiple attributes: + +- Roles: Only show students who have one of the selected roles. +- Only alumni: Only show students who are alumni. +- Students you've suggested for: Only show students for which you made a suggestion. +- Only student coach volunteer: Only show students who indicated that they want to be a student coach. +- Confirmed: Only show students who have one of the selected confirmation statuses. + +These filters can also be reset by clicking the red **Reset filters** button at the top of the student's list. -To delete a project click the **trash** button located on the top right of a project card. A pop up will appear to confirm your decision. Press **Delete** again to delete the project or cancel the delete operation diff --git a/files/user_manual.pdf b/files/user_manual.pdf index daa802363..2752845ac 100644 Binary files a/files/user_manual.pdf and b/files/user_manual.pdf differ diff --git a/files/uses_cases/use_cases_users.md b/files/uses_cases/use_cases_users.md index 1e372d1d1..afb465b90 100644 --- a/files/uses_cases/use_cases_users.md +++ b/files/uses_cases/use_cases_users.md @@ -4,22 +4,22 @@ A new user must create an account in order to use the tool. -| Create Account || -| --- | --- | -| Preconditions | The user has never before created an account and is not known to the tool.
The user is invited by an admin. | -| Postconditions | The user has an account, but this is yet to be approved by an admin.
An email is sent to all admins to request approval.
Admins can see this in the manage users tab. | -| Actors| User | -| Description of steps |
  1. Fill in name
  2. Fill in your email
  3. Fill in your password
  4. Confirm your password
  5. Click a "create account" button
  6. A "hold on tight" window is shown
  7. You receive an email once you've been verified
| -| Alternative flow| The person is the first to ever create an account Signup via Github/Google instead of filling in username/password: | +| Create Account | | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Preconditions | The user has never before created an account and is not known to the tool.
The user is invited by an admin. | +| Postconditions | The user has an account, but this is yet to be approved by an admin.
An email is sent to all admins to request approval.
Admins can see this in the manage users tab. | +| Actors | User | +| Description of steps |
  1. Fill in name
  2. Fill in your email
  3. Fill in your password
  4. Confirm your password
  5. Click a "create account" button
  6. A "hold on tight" window is shown
  7. You receive an email once you've been verified
| +| Alternative flow | The person is the first to ever create an account Signup via Github instead of filling in username/password: | ## Login Every time a recurring user returns, the user must be reauthenticated in order to use the tool. -| Login || -| --- | --- | -| Preconditions | The user has previously created an account. | -| Postconditions | The user has acces to the tool. | -| Actors| User | -| Description of steps |
  1. Fill in your name or email
  2. Fill in your password
  3. Click the login button
  4. The main window is shown, you have access to the tool
| -| Alternative flow| Password incorrect/user unknown: Not yet verified: Login via github instead of filling in username/password: | +| Login | | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Preconditions | The user has previously created an account. | +| Postconditions | The user has acces to the tool. | +| Actors | User | +| Description of steps |
  1. Fill in your name or email
  2. Fill in your password
  3. Click the login button
  4. The main window is shown, you have access to the tool
| +| Alternative flow | Password incorrect/user unknown: Not yet verified: Login via github instead of filling in username/password: | diff --git a/frontend/.env.example b/frontend/.env.example index 7320849c0..d4a3cb26b 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1 +1,3 @@ -REACT_APP_BASE_URL="http://localhost:8000" \ No newline at end of file +REACT_APP_BE_DOMAIN="localhost:8000" +REACT_APP_FE_BASE_URL="http://localhost:3000" +REACT_APP_GITHUB_CLIENT_ID="" \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 99d334d97..c7d9b6e9f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,48 +3,52 @@ "version": "0.1.0", "private": true, "dependencies": { - "@fortawesome/fontawesome-svg-core": "^1.3.0", - "@fortawesome/free-solid-svg-icons": "^6.0.0", + "@fortawesome/fontawesome-svg-core": "^6.1.1", + "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.17", - "axios": "^0.26.1", + "axios": "^0.27.2", "bootstrap": "5.1.3", "buffer": "^6.0.3", + "multiselect-react-dropdown": "^2.0.22", "react": "^17.0.2", - "react-bootstrap": "^2.2.1", + "react-bootstrap": "^2.4.0", "react-bootstrap-typeahead": "^6.0.0-alpha.11", "react-collapsible": "^2.8.4", "react-dom": "^17.0.2", "react-icons": "^4.3.1", "react-infinite-scroller": "^1.2.6", "react-router-bootstrap": "^0.26.1", - "react-router-dom": "^6.2.1", - "react-scripts": "^5.0.0", + "react-router-dom": "^6.3.0", + "react-scripts": "^5.0.1", + "react-select": "^5.3.1", "react-social-login-buttons": "^3.6.0", + "react-toastify": "^9.0.1", "reactjs-popup": "^2.0.5", - "styled-components": "^5.3.3", - "typescript": "^4.4.2", + "styled-components": "^5.3.5", + "typescript": "^4.6.4", "web-vitals": "^2.1.0" }, "devDependencies": { - "@testing-library/jest-dom": "^5.14.1", - "@testing-library/react": "^12.0.0", - "@testing-library/user-event": "^13.2.1", + "@testing-library/jest-dom": "^5.16.4", + "@testing-library/react": "^12.1.5", + "@testing-library/user-event": "^14.2.0", "@types/axios": "^0.14.0", - "@types/enzyme": "^3.10.11", + "@types/enzyme": "^3.10.12", "@types/enzyme-adapter-react-16": "^1.0.6", "@types/jest": "^27.0.1", - "@types/node": "^16.7.13", + "@types/node": "^17.0.34", "@types/react": "^17.0.20", + "@types/react-beautiful-dnd": "^13.1.2", "@types/react-dom": "^17.0.9", "@types/react-infinite-scroller": "^1.2.3", "@types/react-router-bootstrap": "^0.24.5", "@types/react-router-dom": "^5.3.3", - "@types/styled-components": "^5.1.24", - "@typescript-eslint/eslint-plugin": "^5.12.0", - "@typescript-eslint/parser": "^5.12.0", + "@types/styled-components": "^5.1.25", + "@typescript-eslint/eslint-plugin": "^5.25.0", + "@typescript-eslint/parser": "^5.25.0", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.6", - "eslint": "^8.9.0", + "eslint": "^8.15.0", "eslint-config-prettier": "^8.3.0", "eslint-config-standard": "^16.0.3", "eslint-plugin-node": "^11.1.0", @@ -52,9 +56,10 @@ "eslint-plugin-promise": "^6.0.0", "eslint-plugin-react": "^7.28.0", "eslint-plugin-standard": "^5.0.0", - "jest": "^27.5.1", - "prettier": "^2.5.1", - "typedoc": "^0.22.13" + "react-beautiful-dnd": "^13.1.0", + "jest": "^28.1.0", + "prettier": "^2.6.2", + "typedoc": "^0.22.15" }, "scripts": { "start": "react-scripts start", diff --git a/frontend/src/App.css b/frontend/src/App.css index 3f0d1d775..4821d66ae 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,11 +1,19 @@ :root { + /* OSOC colours */ --osoc_blue: #131329; --osoc_orange: #fcb70f; --osoc_red: #f14a3b; --osoc_green: #44dba4; + /* Darkened OSOC colours, used for hovering, etc */ + --osoc_blue_darkened: #0e0e1e; + --osoc_orange_darkened: #c58c02; + --osoc_red_darkened: #d21f0f; + --osoc_green_darkened: #21be85; + /* General site colours */ --react_dark_grey: #343434; --custom_light_blue: #00bfff; --background_color: #272741; + --card-color: #323252; } #root { @@ -23,3 +31,48 @@ body { background-color: var(--background_color); color: white; } + +/* Scrollbar */ +::-webkit-scrollbar { + width: 9px; + height: 7px; +} +::-webkit-scrollbar-button { + width: 0; + height: 0; +} +::-webkit-scrollbar-thumb { + background: #88888d; + border: 0 none #ffffff; + border-radius: 50px; +} +::-webkit-scrollbar-thumb:hover { + background: #bdbdc2; +} +::-webkit-scrollbar-thumb:active { + background: #bdbdc2; +} +::-webkit-scrollbar-track { + background: var(--osoc_blue); + border: 0 none #ffffff; + border-radius: 22px; +} +::-webkit-scrollbar-corner { + background: transparent; +} + +/* All tables */ +.table-dark { + --bs-table-bg: #0f0f30; + --bs-table-background: #0f0f30; + --bs-table-striped-bg: #1a1a38; + --bs-table-striped-color: #fff; + --bs-table-active-bg: #51516e; + --bs-table-active-color: #fff; + --bs-table-hover-bg: #41415e; + --bs-table-hover-color: #fff; +} + +.table > :not(:first-child) { + border-top: none; +} diff --git a/frontend/src/App.styles.ts b/frontend/src/App.styles.ts new file mode 100644 index 000000000..cae6c2e84 --- /dev/null +++ b/frontend/src/App.styles.ts @@ -0,0 +1,25 @@ +import RBContainer from "react-bootstrap/Container"; +import styled from "styled-components"; + +export const Container = styled.div` + min-height: 100vh; + display: flex; + flex-direction: column; +`; + +export const ContentWrapper = styled.div` + flex: 1; +`; + +export const PageContainer = styled(RBContainer).attrs(() => ({ + className: "mt-2", +}))` + display: flex; + flex-direction: column; + justify-content: center; + margin: auto; +`; + +export const CenterText = styled.div` + text-align: center; +`; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cf4a170eb..87578b17c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,8 +1,10 @@ import React from "react"; import "bootstrap/dist/css/bootstrap.min.css"; import "./App.css"; +import "react-toastify/dist/ReactToastify.css"; +import { AuthProvider, SocketProvider } from "./contexts"; +import { ToastContainer } from "react-toastify"; import Router from "./Router"; -import { AuthProvider } from "./contexts"; /** * Main application component. Wraps the [[Router]] in an [[AuthProvider]] so that @@ -12,7 +14,10 @@ export default function App() { return ( // AuthContext should be available in the entire application - + + + + ); } diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 407393242..dddb9edc3 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -1,8 +1,7 @@ import React from "react"; -import { Container, ContentWrapper } from "./app.styles"; +import { Container, ContentWrapper } from "./App.styles"; import { BrowserRouter, Navigate, Outlet, Route, Routes } from "react-router-dom"; - -import { AdminRoute, Footer, Navbar, PrivateRoute } from "./components"; +import { AdminRoute, Footer, Navbar, PrivateRoute, CurrentEditionRoute } from "./components"; import { useAuth } from "./contexts"; import { EditionsPage, @@ -12,14 +11,18 @@ import { ProjectsPage, ProjectDetailPage, CreateProjectPage, - RegisterPage, StudentsPage, UsersPage, AdminsPage, VerifyingTokenPage, + StudentMailHistoryPage, + MailOverviewPage, + StudentInfoPage, } from "./views"; import { ForbiddenPage, NotFoundPage } from "./views/errors"; +import { RedirectPage, RegisterPage } from "./views/Registration"; import { Role } from "./data/enums"; +import { GitHubOAuth } from "./views/OAuth"; /** * Router component to render different pages depending on the current url. Renders @@ -46,8 +49,16 @@ export default function Router() { // the LoginPage } /> + }> + } /> + } + /> + {/* Redirect /login to the login page */} } /> + } /> } /> {/* Catch all routes in a PrivateRoute, so you can't visit them */} {/* unless you are logged in */} @@ -58,17 +69,16 @@ export default function Router() { }> } /> }> - {/* TODO create edition page */} + {/* create edition page */} } /> }> - {/* TODO edition page? do we need? maybe just some nav/links? */} } /> {/* Projects routes */} }> } /> - }> + }> {/* create project page */} } /> @@ -80,11 +90,19 @@ export default function Router() { {/* Students routes */} - } /> - {/* TODO student page */} - } /> - {/* TODO student emails page */} - } /> + }> + } /> + }> + } /> + + } /> + }> + } + /> + + {/* Users routes */} }> diff --git a/frontend/src/app.styles.ts b/frontend/src/app.styles.ts deleted file mode 100644 index 35fce3242..000000000 --- a/frontend/src/app.styles.ts +++ /dev/null @@ -1,11 +0,0 @@ -import styled from "styled-components"; - -export const Container = styled.div` - min-height: 100vh; - display: flex; - flex-direction: column; -`; - -export const ContentWrapper = styled.div` - flex: 1; -`; diff --git a/frontend/src/components/AdminsComponents/AddAdmin.tsx b/frontend/src/components/AdminsComponents/AddAdmin.tsx index e73dbeb69..b364a0328 100644 --- a/frontend/src/components/AdminsComponents/AddAdmin.tsx +++ b/frontend/src/components/AdminsComponents/AddAdmin.tsx @@ -1,28 +1,17 @@ import { getUsersNonAdmin, User } from "../../utils/api/users/users"; -import React, { useState } from "react"; +import { createRef, useEffect, useState } from "react"; import { addAdmin } from "../../utils/api/users/admins"; -import { AddAdminButton, ModalContentConfirm, Warning } from "./styles"; -import { Button, Modal, Spinner } from "react-bootstrap"; +import { AddButtonDiv, EmailDiv, Warning } from "./styles"; +import { Button, Modal } from "react-bootstrap"; import { AsyncTypeahead, Menu } from "react-bootstrap-typeahead"; -import { Error } from "../UsersComponents/Requests/styles"; -import { StyledMenuItem } from "../GeneralComponents/styles"; -import UserMenuItem from "../GeneralComponents/MenuItem"; -import { EmailAndAuth } from "../GeneralComponents"; - -/** - * Warning that the user will get all persmissions. - * @param props.name The name of the user. - */ -function AddWarning(props: { name: string | undefined }) { - if (props.name !== undefined) { - return ( - - Warning: {props.name} will be able to edit/delete all data and manage admin roles. - - ); - } - return null; -} +import { StyledMenuItem } from "../Common/Users/styles"; +import UserMenuItem from "../Common/Users/MenuItem"; +import { EmailAndAuth } from "../Common/Users"; +import CreateButton from "../Common/Buttons/CreateButton"; +import { ModalContentConfirm } from "../Common/styles"; +import Typeahead from "react-bootstrap-typeahead/types/core/Typeahead"; +import { toast } from "react-toastify"; +import { StyledInput } from "../Common/Forms/styles"; /** * Button and popup to add an existing user as admin. @@ -32,31 +21,37 @@ function AddWarning(props: { name: string | undefined }) { export default function AddAdmin(props: { adminAdded: (user: User) => void }) { const [show, setShow] = useState(false); const [selected, setSelected] = useState(undefined); - const [error, setError] = useState(""); - const [loading, setLoading] = useState(false); const [gettingData, setGettingData] = useState(false); // Waiting for data const [users, setUsers] = useState([]); // All users which are not a coach const [searchTerm, setSearchTerm] = useState(""); // The word set in filter + const [clearRef, setClearRef] = useState(false); // The ref must be cleared + + const typeaheadRef = createRef(); + + useEffect(() => { + // For some obscure reason the ref can only be cleared in here & not somewhere else + if (clearRef) { + // This triggers itself, but only once, so it doesn't really matter + setClearRef(false); + typeaheadRef.current?.clear(); + } + }, [clearRef, typeaheadRef]); async function getData(page: number, filter: string | undefined = undefined) { if (filter === undefined) { filter = searchTerm; } setGettingData(true); - setError(""); - try { - const response = await getUsersNonAdmin(filter, page); - if (page === 0) { - setUsers(response.users); - } else { - setUsers(users.concat(response.users)); - } - - setGettingData(false); - } catch (exception) { - setError("Oops, something went wrong..."); - setGettingData(false); + const response = await toast.promise(getUsersNonAdmin(filter, page), { + error: "Failed to retrieve users", + }); + if (page === 0) { + setUsers(response.users); + } else { + setUsers(users.concat(response.users)); } + + setGettingData(false); } function filterData(searchTerm: string) { @@ -67,7 +62,6 @@ export default function AddAdmin(props: { adminAdded: (user: User) => void }) { const handleClose = () => { setSelected(undefined); - setError(""); setShow(false); }; const handleShow = () => { @@ -75,48 +69,34 @@ export default function AddAdmin(props: { adminAdded: (user: User) => void }) { }; async function addUserAsAdmin(user: User) { - setLoading(true); - setError(""); - let success = false; - try { - success = await addAdmin(user.userId); - if (!success) { - setError("Something went wrong. Failed to add admin"); - } - } catch (error) { - setError("Something went wrong. Failed to add admin"); - } - setLoading(false); - if (success) { - props.adminAdded(user); - handleClose(); - } + await toast.promise(addAdmin(user.userId), { + pending: "Adding admin", + success: "Admin successfully added", + error: "Failed to add admin", + }); + + props.adminAdded(user); + setSearchTerm(""); + setSelected(undefined); + setClearRef(true); } - let addButton; - if (loading) { - addButton = ; - } else { - addButton = ( - + let warning; + if (selected !== undefined) { + warning = ( + + Warning: This user will be able to edit/delete all data and manage admin roles. + ); } return ( <> - - Add admin - + + + Add admin + + @@ -132,11 +112,20 @@ export default function AddAdmin(props: { adminAdded: (user: User) => void }) { minLength={1} onSearch={filterData} options={users} - placeholder={"user's name"} + ref={typeaheadRef} + placeholder={"Username"} onChange={selected => { setSelected(selected[0] as User); - setError(""); }} + renderInput={({ inputRef, referenceElementRef, ...inputProps }) => ( + { + inputRef(input); + referenceElementRef(input); + }} + /> + )} renderMenu={(results, menuProps) => { const { newSelectionPrefix, @@ -163,15 +152,26 @@ export default function AddAdmin(props: { adminAdded: (user: User) => void }) { ); }} /> - - + + + + {warning} - {addButton} + { + if (selected !== undefined) { + addUserAsAdmin(selected); + } + }} + disabled={selected === undefined} + > + Add admin + - {error} diff --git a/frontend/src/components/AdminsComponents/AdminList.tsx b/frontend/src/components/AdminsComponents/AdminList.tsx index b383c3de6..a4b0a7e96 100644 --- a/frontend/src/components/AdminsComponents/AdminList.tsx +++ b/frontend/src/components/AdminsComponents/AdminList.tsx @@ -1,58 +1,44 @@ import { User } from "../../utils/api/users/users"; -import { SpinnerContainer } from "../UsersComponents/Requests/styles"; -import { Spinner } from "react-bootstrap"; import { AdminsTable } from "./styles"; import React from "react"; import { AdminListItem } from "./index"; +import { ListDiv } from "../Common/Users/styles"; +import { RemoveTh } from "../Common/Tables/styles"; /** * List of [[AdminListItem]]s which represents all admins. * @param props.admins List of all users who are admin. * @param props.loading Data is being fetched. * @param props.gotData Data is received. - * @param props.refresh Function which will be called after deleting an admin. + * @param props.removeAdmin Function which will be called after deleting an admin. * @constructor */ export default function AdminList(props: { admins: User[]; loading: boolean; gotData: boolean; - refresh: () => void; - getMoreAdmins: (page: number) => void; - moreAdminsAvailable: boolean; + removeAdmin: (user: User) => void; }) { - if (props.loading) { - return ( - - - - ); - } else if (props.admins.length === 0) { - if (props.gotData) { - return
No admins
; - } else { - return null; - } - } - - const body = ( - - {props.admins.map(admin => ( - - ))} - - ); - return ( - - - - Name - Email - Remove - - - {body} - + + + + + Name + Email + Remove + + + + {props.admins.map(admin => ( + + ))} + + + ); } diff --git a/frontend/src/components/AdminsComponents/AdminListItem.tsx b/frontend/src/components/AdminsComponents/AdminListItem.tsx index 5b081916e..2d7a5ea03 100644 --- a/frontend/src/components/AdminsComponents/AdminListItem.tsx +++ b/frontend/src/components/AdminsComponents/AdminListItem.tsx @@ -1,23 +1,24 @@ import { User } from "../../utils/api/users/users"; import React from "react"; import { RemoveAdmin } from "./index"; -import { EmailAndAuth } from "../GeneralComponents"; +import { EmailAndAuth } from "../Common/Users"; +import { RemoveTd } from "../Common/Tables/styles"; /** * An item from [[AdminList]]. Contains the credentials of an admin and a button to remove the admin. * @param props.admin The user which is represented. - * @param props.refresh A function which will be called after removing an admin. + * @param props.removeAdmin A function which will be called after removing an admin. */ -export default function AdminItem(props: { admin: User; refresh: () => void }) { +export default function AdminItem(props: { admin: User; removeAdmin: (user: User) => void }) { return ( {props.admin.name} - - - + + + ); } diff --git a/frontend/src/components/AdminsComponents/RemoveAdmin.tsx b/frontend/src/components/AdminsComponents/RemoveAdmin.tsx index e62ccefba..962815015 100644 --- a/frontend/src/components/AdminsComponents/RemoveAdmin.tsx +++ b/frontend/src/components/AdminsComponents/RemoveAdmin.tsx @@ -2,48 +2,47 @@ import { User } from "../../utils/api/users/users"; import React, { useState } from "react"; import { removeAdmin, removeAdminAndCoach } from "../../utils/api/users/admins"; import { Button, Modal } from "react-bootstrap"; -import { ModalContentWarning } from "./styles"; -import { Error } from "../UsersComponents/Requests/styles"; +import { RemoveAdminBody } from "./styles"; +import { ModalContentWarning } from "../Common/styles"; +import { toast } from "react-toastify"; +import DeleteButton from "../Common/Buttons/DeleteButton"; /** * Button and popup to remove a user as admin (and as coach). * @param props.admin The user which can be removed. - * @param props.refresh A function which is called when the user is removed as admin. + * @param props.removeAdmin A function which is called when the user is removed as admin. */ -export default function RemoveAdmin(props: { admin: User; refresh: () => void }) { +export default function RemoveAdmin(props: { admin: User; removeAdmin: (user: User) => void }) { const [show, setShow] = useState(false); - const [error, setError] = useState(""); const handleClose = () => setShow(false); const handleShow = () => { setShow(true); - setError(""); }; - async function removeUserAsAdmin(userId: number, removeCoach: boolean) { - try { - let removed; - if (removeCoach) { - removed = await removeAdminAndCoach(userId); - } else { - removed = await removeAdmin(userId); - } - - if (removed) { - props.refresh(); - } else { - setError("Something went wrong. Failed to remove admin"); - } - } catch (error) { - setError("Something went wrong. Failed to remove admin"); + async function removeUserAsAdmin(removeCoach: boolean) { + if (removeCoach) { + await toast.promise(removeAdminAndCoach(props.admin.userId), { + pending: "Removing admin", + success: "Admin successfully removed", + error: "Failed to remove admin", + }); + } else { + await toast.promise(removeAdmin(props.admin.userId), { + pending: "Removing admin", + success: "Admin successfully removed", + error: "Failed to remove admin", + }); } + + props.removeAdmin(props.admin); } return ( <> - + @@ -51,39 +50,32 @@ export default function RemoveAdmin(props: { admin: User; refresh: () => void }) Remove Admin -

{props.admin.name}

-

{props.admin.auth.email}

-

- Remove admin: {props.admin.name} will stay coach for assigned editions -

+ +

{props.admin.name}

+

{props.admin.auth.email}

+

Remove admin: This admin will stay coach for assigned editions

+
- - + - {error}
diff --git a/frontend/src/components/AdminsComponents/styles.ts b/frontend/src/components/AdminsComponents/styles.ts index 394500255..1d6860ab2 100644 --- a/frontend/src/components/AdminsComponents/styles.ts +++ b/frontend/src/components/AdminsComponents/styles.ts @@ -1,24 +1,30 @@ import styled from "styled-components"; -import { Button, Table } from "react-bootstrap"; +import { Table } from "react-bootstrap"; export const Warning = styled.div` color: var(--osoc_red); `; -export const AdminsTable = styled(Table)``; +export const AdminsTable = styled(Table).attrs({ + striped: true, + bordered: true, + variant: "dark", + hover: false, +})` + width: 45em; + max-width: 100%; + margin-top: 10px; +`; -export const ModalContentConfirm = styled.div` - border: 3px solid var(--osoc_green); - background-color: var(--osoc_blue); +export const EmailDiv = styled.div` + overflow: auto; `; -export const ModalContentWarning = styled.div` - border: 3px solid var(--osoc_red); - background-color: var(--osoc_blue); +export const RemoveAdminBody = styled.div` + overflow: hidden; `; -export const AddAdminButton = styled(Button).attrs({ - size: "sm", -})` +export const AddButtonDiv = styled.div` float: right; + margin-bottom: 5px; `; diff --git a/frontend/src/components/Common/Buttons/BackButton.tsx b/frontend/src/components/Common/Buttons/BackButton.tsx new file mode 100644 index 000000000..276b08a83 --- /dev/null +++ b/frontend/src/components/Common/Buttons/BackButton.tsx @@ -0,0 +1,11 @@ +import { GoBack } from "./styles"; +import { BiArrowBack } from "react-icons/bi"; + +export default function BackButton(props: { label: string; onClick: () => void }) { + return ( + + + {props.label} + + ); +} diff --git a/frontend/src/components/Common/Buttons/CreateButton.tsx b/frontend/src/components/Common/Buttons/CreateButton.tsx new file mode 100644 index 000000000..00f3c5686 --- /dev/null +++ b/frontend/src/components/Common/Buttons/CreateButton.tsx @@ -0,0 +1,26 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { BasicButton } from "./props"; +import { GreenButton } from "./styles"; + +/** + * Green button with a "+"-icon + * Changes to orange on hover because the OSOC-site does this too + */ +export default function CreateButton({ + label = "", + showIcon = true, + children, + ...props +}: BasicButton) { + return ( + + {showIcon && ( + + )} + {children} + {label} + + ); +} diff --git a/frontend/src/components/Common/Buttons/DeleteButton.tsx b/frontend/src/components/Common/Buttons/DeleteButton.tsx new file mode 100644 index 000000000..42acab75a --- /dev/null +++ b/frontend/src/components/Common/Buttons/DeleteButton.tsx @@ -0,0 +1,25 @@ +import { BasicButton } from "./props"; +import { RedButton } from "./styles"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faTrashCan } from "@fortawesome/free-solid-svg-icons/faTrashCan"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; + +/** + * Red button with a garbage can icon + */ +export default function DeleteButton({ + label = "", + showIcon = true, + children, + ...props +}: BasicButton) { + return ( + + {showIcon && ( + + )} + {children} + {label} + + ); +} diff --git a/frontend/src/components/Common/Buttons/OrangeButton.tsx b/frontend/src/components/Common/Buttons/OrangeButton.tsx new file mode 100644 index 000000000..5924e338c --- /dev/null +++ b/frontend/src/components/Common/Buttons/OrangeButton.tsx @@ -0,0 +1,19 @@ +import { BasicButton } from "./props"; +import { OrangeButton as StyledOrangeButton } from "./styles"; + +/** + * Orange button + */ +export default function OrangeButton({ + label = "", + showIcon = false, + children, + ...props +}: BasicButton) { + return ( + + {children} + {label} + + ); +} diff --git a/frontend/src/components/Common/Buttons/WarningButton.tsx b/frontend/src/components/Common/Buttons/WarningButton.tsx new file mode 100644 index 000000000..bd639160e --- /dev/null +++ b/frontend/src/components/Common/Buttons/WarningButton.tsx @@ -0,0 +1,29 @@ +import { AnimatedButton } from "./props"; +import { RedButton } from "./styles"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons/faTriangleExclamation"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; + +/** + * Red button with a warning triangle icon + */ +export default function WarningButton({ + label = "", + showIcon = true, + animated = "false", + children, + ...props +}: AnimatedButton) { + return ( + + {showIcon && ( + + )} + {children} + {label} + + ); +} diff --git a/frontend/src/components/Common/Buttons/buttonsStyles.css b/frontend/src/components/Common/Buttons/buttonsStyles.css new file mode 100644 index 000000000..3614d98cf --- /dev/null +++ b/frontend/src/components/Common/Buttons/buttonsStyles.css @@ -0,0 +1,10 @@ +.show > .btn-primary.dropdown-toggle { + background-color: var(--osoc_orange); + border-color: var(--osoc_orange); + color: var(--osoc_blue); + box-shadow: none !important; +} + +.dropdown-menu { + width: 100%; +} diff --git a/frontend/src/components/Common/Buttons/index.ts b/frontend/src/components/Common/Buttons/index.ts new file mode 100644 index 000000000..85cf45cb7 --- /dev/null +++ b/frontend/src/components/Common/Buttons/index.ts @@ -0,0 +1,4 @@ +export { default as CreateButton } from "./CreateButton"; +export { default as DeleteButton } from "./DeleteButton"; +export { default as OrangeButton } from "./OrangeButton"; +export { default as WarningButton } from "./WarningButton"; diff --git a/frontend/src/components/Common/Buttons/props.ts b/frontend/src/components/Common/Buttons/props.ts new file mode 100644 index 000000000..e092dfc41 --- /dev/null +++ b/frontend/src/components/Common/Buttons/props.ts @@ -0,0 +1,12 @@ +import { ButtonProps } from "react-bootstrap/Button"; +import React from "react"; + +export interface BasicButton extends ButtonProps { + label?: string; + showIcon?: boolean; + children?: React.ReactNode; +} + +export interface AnimatedButton extends BasicButton { + animated?: string; +} diff --git a/frontend/src/components/Common/Buttons/styles.ts b/frontend/src/components/Common/Buttons/styles.ts new file mode 100644 index 000000000..b9b4aac5e --- /dev/null +++ b/frontend/src/components/Common/Buttons/styles.ts @@ -0,0 +1,149 @@ +import styled, { css } from "styled-components"; + +import { HoverAnimation } from "../styles"; +import { AnimatedButton } from "./props"; +import { Dropdown, DropdownButton, Button } from "react-bootstrap"; + +export const GreenButton = styled(Button)` + ${HoverAnimation}; + + background-color: var(--osoc_green); + border-color: var(--osoc_green); + color: var(--osoc_blue); + + &:disabled { + background-color: #3a6453; + border-color: #3a6453; + color: white; + } + + &:hover, + &:active, + &:focus { + background-color: var(--osoc_orange); + border-color: var(--osoc_orange); + color: var(--osoc_blue); + box-shadow: none !important; + } +`; + +export const OrangeButton = styled(Button)` + ${HoverAnimation}; + + background-color: var(--osoc_orange); + border-color: var(--osoc_orange); + color: var(--osoc_blue); + + &:hover, + &:active, + &:focus { + background-color: var(--osoc_orange_darkened); + border-color: var(--osoc_orange_darkened); + color: var(--osoc_blue); + box-shadow: none !important; + } +`; + +export const DropdownToggle = styled(Dropdown.Toggle)` + ${HoverAnimation}; + + background-color: var(--osoc_green); + border-color: var(--osoc_green); + color: var(--osoc_blue); + + &:disabled { + background-color: var(--osoc_green); + border-color: var(--osoc_green); + color: var(--osoc_blue); + } + + &:hover, + &:active, + &:focus { + background-color: var(--osoc_orange) !important; + border-color: var(--osoc_orange); + color: var(--osoc_blue); + box-shadow: none !important; + } +`; + +const WarningColourAnimation = css` + animation: button-change-colour infinite 3s ease-in-out; + + @keyframes button-change-colour { + 0% { + background-color: var(--osoc_red); + border-color: var(--osoc_red); + + box-shadow: none; + } + + 50% { + background-color: #ff9089ff; + border-color: #ff9089ff; + + box-shadow: 0 0 30px var(--osoc_red_darkened); + } + + 100% { + background-color: var(--osoc_red); + border-color: var(--osoc_red); + + box-shadow: none; + } + } +`; + +export const RedButton = styled(Button)` + ${HoverAnimation}; + + ${props => props.animated && WarningColourAnimation}; + + background-color: var(--osoc_red); + border-color: var(--osoc_red); + + &:disabled { + background-color: #8a4944; + border-color: #8a4944; + } + + &:hover, + &:active, + &:focus { + background-color: var(--osoc_red_darkened); + border-color: var(--osoc_red_darkened); + box-shadow: none !important; + } +`; + +export const CommonDropdownButton = styled(DropdownButton).attrs({ + menuVariant: "dark", +})` + & > Button { + ${HoverAnimation}; + + background-color: var(--osoc_green); + border-color: var(--osoc_green); + color: var(--osoc_blue); + + &:hover, + &:active, + &:focus { + background-color: var(--osoc_orange); + border-color: var(--osoc_orange); + color: var(--osoc_blue); + box-shadow: none !important; + } + } +`; + +export const GoBack = styled.div` + display: flex; + align-items: center; + margin-bottom: 10px; + max-width: max-content; + + :hover { + cursor: pointer; + } +`; diff --git a/frontend/src/components/Common/Forms/CommonMultiselect.tsx b/frontend/src/components/Common/Forms/CommonMultiselect.tsx new file mode 100644 index 000000000..ee0883e69 --- /dev/null +++ b/frontend/src/components/Common/Forms/CommonMultiselect.tsx @@ -0,0 +1,7 @@ +import { StyledMultiSelect } from "./styles"; +import { IMultiselectProps } from "multiselect-react-dropdown/dist/multiselect/interface"; +import "./formsStyles.css"; + +export default function CommonMultiselect(props: IMultiselectProps) { + return ; +} diff --git a/frontend/src/components/Common/Forms/FormControl.tsx b/frontend/src/components/Common/Forms/FormControl.tsx new file mode 100644 index 000000000..6736e79f6 --- /dev/null +++ b/frontend/src/components/Common/Forms/FormControl.tsx @@ -0,0 +1,9 @@ +import { FormControlProps } from "react-bootstrap/FormControl"; +import { StyledFormControl } from "./styles"; + +/** + * An styled version of Bootstrap's Form.Control + */ +export default function FormControl(props: FormControlProps) { + return ; +} diff --git a/frontend/src/components/Common/Forms/SearchBar.tsx b/frontend/src/components/Common/Forms/SearchBar.tsx new file mode 100644 index 000000000..b0a65de42 --- /dev/null +++ b/frontend/src/components/Common/Forms/SearchBar.tsx @@ -0,0 +1,6 @@ +import { FormControlProps } from "react-bootstrap/FormControl"; +import { StyledSearchBar } from "./styles"; + +export default function SearchBar(props: FormControlProps) { + return ; +} diff --git a/frontend/src/components/Common/Forms/formsStyles.css b/frontend/src/components/Common/Forms/formsStyles.css new file mode 100644 index 000000000..b28b8be4f --- /dev/null +++ b/frontend/src/components/Common/Forms/formsStyles.css @@ -0,0 +1,45 @@ +.optionListContainer { + background-color: var(--osoc_blue); + color: white; +} + +.searchWrapper { + border: 2px solid #323252; +} + +.searchWrapper:hover { + border: 2px solid var(--osoc_green); +} + +.searchWrapper:active { + border: 2px solid var(--osoc_green); +} + +.multiSelectContainer input { + color: white; +} + +.icon_down_dir { + filter: invert(70%); +} + +.multiSelectContainer li:hover { + background-color: #363649 !important; +} + +.multiSelectContainer ul { + border: none; +} + +.chip { + background-color: var(--osoc_green); + color: var(--osoc_blue); +} + +.icon_cancel { + filter: invert(80%); +} + +.highlightOption { + background-color: transparent; +} diff --git a/frontend/src/components/Common/Forms/index.ts b/frontend/src/components/Common/Forms/index.ts new file mode 100644 index 000000000..c5c66f801 --- /dev/null +++ b/frontend/src/components/Common/Forms/index.ts @@ -0,0 +1,3 @@ +export { default as FormControl } from "./FormControl"; +export { default as SearchBar } from "./SearchBar"; +export { default as CommonMultiselect } from "./CommonMultiselect"; diff --git a/frontend/src/components/Common/Forms/styles.ts b/frontend/src/components/Common/Forms/styles.ts new file mode 100644 index 000000000..33d81d2a5 --- /dev/null +++ b/frontend/src/components/Common/Forms/styles.ts @@ -0,0 +1,67 @@ +import styled from "styled-components"; +import Form from "react-bootstrap/Form"; +import Multiselect from "multiselect-react-dropdown"; +import { Input } from "react-bootstrap-typeahead"; + +export const StyledFormControl = styled(Form.Control)` + background-color: var(--osoc_blue); + color: white; + border-color: transparent; + + &:focus { + background-color: var(--osoc_blue); + color: white; + border-color: var(--osoc_green); + box-shadow: none; + } + + &:invalid { + border-color: var(--osoc_red); + box-shadow: none; + } +`; + +export const StyledSearchBar = styled(Form.Control)` + background-color: var(--osoc_blue); + color: white; + border: 2px solid #323252; + + &:focus, + &:hover, + &:active { + background-color: var(--osoc_blue); + color: white; + border-color: var(--osoc_green); + box-shadow: none; + } + + &:invalid { + border-color: var(--osoc_red); + box-shadow: none; + } +`; + +export const StyledInput = styled(Input)` + background-color: var(--osoc_blue); + color: white; + border: 2px solid #323252; + width: 100%; + height: 2.5em; + padding: 5px; + box-shadow: none; + outline: none; + border-radius: 5px; + + &:focus { + border: 2px solid var(--osoc_green); + } + + &:invalid { + border: 2px solid var(--osoc_red); + } +`; + +export const StyledMultiSelect = styled(Multiselect).attrs({ variant: "dark" })` + background-color: var(--osoc_blue); + color: white; +`; diff --git a/frontend/src/components/CommonComps/LoadSpinner/LoadSpinner.tsx b/frontend/src/components/Common/LoadSpinner/LoadSpinner.tsx similarity index 100% rename from frontend/src/components/CommonComps/LoadSpinner/LoadSpinner.tsx rename to frontend/src/components/Common/LoadSpinner/LoadSpinner.tsx diff --git a/frontend/src/components/CommonComps/LoadSpinner/index.ts b/frontend/src/components/Common/LoadSpinner/index.ts similarity index 100% rename from frontend/src/components/CommonComps/LoadSpinner/index.ts rename to frontend/src/components/Common/LoadSpinner/index.ts diff --git a/frontend/src/components/CommonComps/LoadSpinner/styles.ts b/frontend/src/components/Common/LoadSpinner/styles.ts similarity index 100% rename from frontend/src/components/CommonComps/LoadSpinner/styles.ts rename to frontend/src/components/Common/LoadSpinner/styles.ts diff --git a/frontend/src/components/Common/Placeholders/OSOCSpinner/OSOCSpinner.tsx b/frontend/src/components/Common/Placeholders/OSOCSpinner/OSOCSpinner.tsx new file mode 100644 index 000000000..e78d8a0ec --- /dev/null +++ b/frontend/src/components/Common/Placeholders/OSOCSpinner/OSOCSpinner.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { SpinningC } from "./styles"; +import OsocLetterC from "../../../../images/letters/osoc_c.svg"; + +interface Props { + children?: React.ReactNode; + show: boolean; +} + +/** + * Loading spinner comprised of a spinning C from the OSOC logo, as on the OSOC-website. + * This component can render children to easily hide a component if necessary + */ +export default function OSOCSpinner({ children, show }: Props) { + if (show) { + return ; + } + + return <>{children}; +} diff --git a/frontend/src/components/Common/Placeholders/OSOCSpinner/index.ts b/frontend/src/components/Common/Placeholders/OSOCSpinner/index.ts new file mode 100644 index 000000000..4d9e492be --- /dev/null +++ b/frontend/src/components/Common/Placeholders/OSOCSpinner/index.ts @@ -0,0 +1 @@ +export { default as OSOCSpinner } from "./OSOCSpinner"; diff --git a/frontend/src/components/Common/Placeholders/OSOCSpinner/styles.ts b/frontend/src/components/Common/Placeholders/OSOCSpinner/styles.ts new file mode 100644 index 000000000..46178e7b4 --- /dev/null +++ b/frontend/src/components/Common/Placeholders/OSOCSpinner/styles.ts @@ -0,0 +1,19 @@ +import styled from "styled-components"; + +export const SpinningC = styled.img.attrs(() => ({ + height: "40px", + width: "auto", + alt: "Loading...", +}))` + animation: rotate-c infinite 5s linear; + + @keyframes rotate-c { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } + } +`; diff --git a/frontend/src/components/Common/Placeholders/index.ts b/frontend/src/components/Common/Placeholders/index.ts new file mode 100644 index 000000000..6e656ea5d --- /dev/null +++ b/frontend/src/components/Common/Placeholders/index.ts @@ -0,0 +1 @@ +export { OSOCSpinner } from "./OSOCSpinner"; diff --git a/frontend/src/components/Common/Tables/index.ts b/frontend/src/components/Common/Tables/index.ts new file mode 100644 index 000000000..9ad2f8fa2 --- /dev/null +++ b/frontend/src/components/Common/Tables/index.ts @@ -0,0 +1 @@ +export { StyledTable as Table } from "./styles"; diff --git a/frontend/src/components/Common/Tables/styles.ts b/frontend/src/components/Common/Tables/styles.ts new file mode 100644 index 000000000..e16b7a138 --- /dev/null +++ b/frontend/src/components/Common/Tables/styles.ts @@ -0,0 +1,27 @@ +import styled from "styled-components"; +import Table from "react-bootstrap/Table"; + +export const StyledTable = styled(Table).attrs({ + striped: true, + bordered: true, + variant: "dark", + hover: false, +})``; + +export const RemoveTh = styled.th` + width: 200px; + text-align: center; +`; + +export const RemoveTd = styled.td` + text-align: center; + vertical-align: middle; +`; + +export const DateTh = styled.th` + text-align: center; +`; + +export const DateTd = styled.td` + text-align: center; +`; diff --git a/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx b/frontend/src/components/Common/Users/AuthTypeIcon.tsx similarity index 64% rename from frontend/src/components/GeneralComponents/AuthTypeIcon.tsx rename to frontend/src/components/Common/Users/AuthTypeIcon.tsx index 9f38b8d1a..0fe9bb797 100644 --- a/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx +++ b/frontend/src/components/Common/Users/AuthTypeIcon.tsx @@ -1,6 +1,6 @@ import { HiOutlineMail } from "react-icons/hi"; -import { AiFillGithub, AiFillGoogleCircle, AiOutlineQuestionCircle } from "react-icons/ai"; -import { AuthType } from "../../data/enums"; +import { AiFillGithub, AiOutlineQuestionCircle } from "react-icons/ai"; +import { AuthType } from "../../../data/enums"; /** * An icon representing the type of authentication @@ -11,8 +11,6 @@ export default function AuthTypeIcon(props: { type: AuthType }) { return ; case AuthType.GitHub: return ; - case AuthType.Google: - return ; } return ; } diff --git a/frontend/src/components/GeneralComponents/EmailAndAuth.tsx b/frontend/src/components/Common/Users/EmailAndAuth.tsx similarity index 91% rename from frontend/src/components/GeneralComponents/EmailAndAuth.tsx rename to frontend/src/components/Common/Users/EmailAndAuth.tsx index 61fe38a72..6e54dc855 100644 --- a/frontend/src/components/GeneralComponents/EmailAndAuth.tsx +++ b/frontend/src/components/Common/Users/EmailAndAuth.tsx @@ -1,6 +1,6 @@ import { AuthTypeDiv, EmailAndAuthDiv, EmailDiv } from "./styles"; import AuthTypeIcon from "./AuthTypeIcon"; -import { User } from "../../utils/api/users/users"; +import { User } from "../../../utils/api/users/users"; /** * Email adress + auth type icon of a given user. diff --git a/frontend/src/components/GeneralComponents/MenuItem.tsx b/frontend/src/components/Common/Users/MenuItem.tsx similarity index 69% rename from frontend/src/components/GeneralComponents/MenuItem.tsx rename to frontend/src/components/Common/Users/MenuItem.tsx index 849fd4fea..beb6969de 100644 --- a/frontend/src/components/GeneralComponents/MenuItem.tsx +++ b/frontend/src/components/Common/Users/MenuItem.tsx @@ -1,5 +1,5 @@ -import { User } from "../../utils/api/users/users"; -import { EmailDiv, NameDiv } from "./styles"; +import { User } from "../../../utils/api/users/users"; +import { DropdownEmailDiv, NameDiv } from "./styles"; import EmailAndAuth from "./EmailAndAuth"; /** @@ -10,9 +10,9 @@ export default function UserMenuItem(props: { user: User }) { return (
{props.user.name} - + - +
); } diff --git a/frontend/src/components/GeneralComponents/index.ts b/frontend/src/components/Common/Users/index.ts similarity index 100% rename from frontend/src/components/GeneralComponents/index.ts rename to frontend/src/components/Common/Users/index.ts diff --git a/frontend/src/components/GeneralComponents/styles.ts b/frontend/src/components/Common/Users/styles.ts similarity index 51% rename from frontend/src/components/GeneralComponents/styles.ts rename to frontend/src/components/Common/Users/styles.ts index 6c570862f..fcfb5ed99 100644 --- a/frontend/src/components/GeneralComponents/styles.ts +++ b/frontend/src/components/Common/Users/styles.ts @@ -2,6 +2,8 @@ import styled from "styled-components"; import { MenuItem } from "react-bootstrap-typeahead"; export const StyledMenuItem = styled(MenuItem)` + padding-top: 0; + padding-bottom: 0; color: white; transition: 200ms ease-out; @@ -12,12 +14,10 @@ export const StyledMenuItem = styled(MenuItem)` } `; -export const NameDiv = styled.div` - float: left; -`; +export const NameDiv = styled.div``; export const EmailDiv = styled.div` - float: right; + float: left; `; export const AuthTypeDiv = styled.div` @@ -26,5 +26,28 @@ export const AuthTypeDiv = styled.div` `; export const EmailAndAuthDiv = styled.div` - width: fit-content; + width: max-content; +`; + +export const DropdownEmailDiv = styled.div` + font-size: small; + margin-bottom: 5px; +`; + +export const ListDiv = styled.div` + width: 100%; + height: fit-content; + max-height: 40em; + overflow: auto; + margin-top: 10px; +`; + +export const SearchFieldDiv = styled.div` + float: left; + width: 15em; +`; + +export const TableDiv = styled.div` + width: 100%; + clear: left; `; diff --git a/frontend/src/components/Common/index.ts b/frontend/src/components/Common/index.ts new file mode 100644 index 000000000..65b58d958 --- /dev/null +++ b/frontend/src/components/Common/index.ts @@ -0,0 +1,6 @@ +export * as Buttons from "./Buttons"; +export * as Forms from "./Forms"; +export { default as LoadSpinner } from "./LoadSpinner"; +export * as Placeholders from "./Placeholders"; +export * as Tables from "./Tables"; +export * as Users from "./Users"; diff --git a/frontend/src/components/Common/styles.ts b/frontend/src/components/Common/styles.ts new file mode 100644 index 000000000..55f0efa75 --- /dev/null +++ b/frontend/src/components/Common/styles.ts @@ -0,0 +1,20 @@ +import styled, { css } from "styled-components"; + +// Css for a component that does an animation on hover +export const HoverAnimation = css` + transition: 200ms ease-out; + + &:hover { + transition: 200ms ease-in; + } +`; + +export const ModalContentConfirm = styled.div` + border: 3px solid var(--osoc_green); + background-color: var(--osoc_blue); +`; + +export const ModalContentWarning = styled.div` + border: 3px solid var(--osoc_red); + background-color: var(--osoc_blue); +`; diff --git a/frontend/src/components/CommonComps/index.ts b/frontend/src/components/CommonComps/index.ts deleted file mode 100644 index 507a08be1..000000000 --- a/frontend/src/components/CommonComps/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as LoadSpinner } from "./LoadSpinner"; diff --git a/frontend/src/components/CurrentEditionRoute/CurrentEditionRoute.tsx b/frontend/src/components/CurrentEditionRoute/CurrentEditionRoute.tsx new file mode 100644 index 000000000..be2d5a749 --- /dev/null +++ b/frontend/src/components/CurrentEditionRoute/CurrentEditionRoute.tsx @@ -0,0 +1,31 @@ +import { Navigate, Outlet, useParams } from "react-router-dom"; +import { useAuth } from "../../contexts"; +import { Role } from "../../data/enums"; +import { isReadonlyEdition } from "../../utils/logic"; + +/** + * React component for editable editions and admin-only routes. + * Redirects to the [[LoginPage]] (status 401) if not authenticated, + * and to the [[ForbiddenPage]] (status 403) if not admin or read-only. + * + * Example usage: + * ```ts + * }> + * // These routes will only render if the user is an admin and is not on a read-only edition + * + * + * + * ``` + */ +export default function CurrentEditionRoute() { + const { isLoggedIn, role, editions } = useAuth(); + const params = useParams(); + const editionId = params.editionId; + return !isLoggedIn ? ( + + ) : role === Role.COACH || isReadonlyEdition(editionId, editions) ? ( + + ) : ( + + ); +} diff --git a/frontend/src/components/CurrentEditionRoute/index.ts b/frontend/src/components/CurrentEditionRoute/index.ts new file mode 100644 index 000000000..f760809b8 --- /dev/null +++ b/frontend/src/components/CurrentEditionRoute/index.ts @@ -0,0 +1 @@ +export { default } from "./CurrentEditionRoute"; diff --git a/frontend/src/components/EditionsPage/DeleteEditionButton.tsx b/frontend/src/components/EditionsPage/DeleteEditionButton.tsx index 298464b7e..ca23830ad 100644 --- a/frontend/src/components/EditionsPage/DeleteEditionButton.tsx +++ b/frontend/src/components/EditionsPage/DeleteEditionButton.tsx @@ -26,7 +26,7 @@ export default function DeleteEditionButton(props: Props) { } return ( - + Delete this edition diff --git a/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx index f472d8b1a..dbd8e6d75 100644 --- a/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx +++ b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx @@ -8,6 +8,7 @@ import InfoMessage from "./InfoMessage"; import Spinner from "react-bootstrap/Spinner"; import { deleteEdition } from "../../../utils/api/editions"; import { getCurrentEdition, setCurrentEdition } from "../../../utils/session-storage"; +import { SearchBar } from "../../Common/Forms"; interface Props { edition: Edition; @@ -111,10 +112,9 @@ export default function DeleteEditionModal(props: Props) { Type the name of the edition in the field below - handleTextfieldChange(e.target.value)} /> diff --git a/frontend/src/components/EditionsPage/EditionRow.tsx b/frontend/src/components/EditionsPage/EditionRow.tsx index aba87e499..18fd757ee 100644 --- a/frontend/src/components/EditionsPage/EditionRow.tsx +++ b/frontend/src/components/EditionsPage/EditionRow.tsx @@ -1,9 +1,13 @@ import { Edition } from "../../data/interfaces"; import DeleteEditionButton from "./DeleteEditionButton"; import { RowContainer } from "./styles"; +import MarkReadonlyButton from "./MarkReadonlyButton"; +import React from "react"; +import Col from "react-bootstrap/Col"; interface Props { edition: Edition; + handleClick: (edition: Edition) => Promise; } /** @@ -13,11 +17,21 @@ export default function EditionRow(props: Props) { return ( -
-

{props.edition.name}

- {props.edition.year} -
- + +
+

{props.edition.name}

+ {props.edition.year} +
+ + + await props.handleClick(props.edition)} + /> + + + +
); diff --git a/frontend/src/components/EditionsPage/EditionsTable.tsx b/frontend/src/components/EditionsPage/EditionsTable.tsx index c4284f66b..154a36537 100644 --- a/frontend/src/components/EditionsPage/EditionsTable.tsx +++ b/frontend/src/components/EditionsPage/EditionsTable.tsx @@ -1,8 +1,12 @@ import React, { useEffect, useState } from "react"; -import { StyledTable, LoadingSpinner } from "./styles"; -import { getEditions } from "../../utils/api/editions"; +import { LoadingSpinner, StyledTable } from "./styles"; +import { getEditions, patchEdition } from "../../utils/api/editions"; import EditionRow from "./EditionRow"; import EmptyEditionsTableMessage from "./EmptyEditionsTableMessage"; +import { Edition } from "../../data/interfaces"; +import { toast } from "react-toastify"; +import { updateEditionState, useAuth } from "../../contexts"; +import { Role } from "../../data/enums"; /** * Table on the [[EditionsPage]] that renders a list of all editions @@ -11,14 +15,30 @@ import EmptyEditionsTableMessage from "./EmptyEditionsTableMessage"; * If the user is an admin, this will also render a delete button. */ export default function EditionsTable() { + const authCtx = useAuth(); const [loading, setLoading] = useState(true); const [rows, setRows] = useState([]); + async function handleClick(edition: Edition) { + if (authCtx.role !== Role.ADMIN) return; + + await toast.promise(async () => await patchEdition(edition.name, !edition.readonly), { + pending: "Changing edition status", + error: "Error changing status", + success: `Successfully changed status to ${ + edition.readonly ? "editable" : "read-only" + }.`, + }); + + updateEditionState(authCtx, edition); + await loadEditions(); + } + async function loadEditions() { const response = await getEditions(); const newRows: React.ReactNode[] = response.editions.map(edition => ( - + )); setRows(newRows); @@ -27,6 +47,7 @@ export default function EditionsTable() { useEffect(() => { loadEditions(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Still loading: display a spinner instead diff --git a/frontend/src/components/EditionsPage/MarkReadonlyButton.tsx b/frontend/src/components/EditionsPage/MarkReadonlyButton.tsx new file mode 100644 index 000000000..bb43e7e28 --- /dev/null +++ b/frontend/src/components/EditionsPage/MarkReadonlyButton.tsx @@ -0,0 +1,28 @@ +import { Edition } from "../../data/interfaces"; +import { StyledReadonlyText } from "./styles"; +import { useAuth } from "../../contexts"; +import { Role } from "../../data/enums"; + +interface Props { + edition: Edition; + handleClick: () => void; +} + +/** + * Button on the [[EditionsPage]], displayed in an [[EditionsRow]], to toggle the readonly + * state of an edition. + */ +export default function MarkReadonlyButton({ edition, handleClick }: Props) { + const { role } = useAuth(); + const label = edition.readonly ? "READ-ONLY" : "EDITABLE"; + + return ( + + {label} + + ); +} diff --git a/frontend/src/components/EditionsPage/NewEditionButton.tsx b/frontend/src/components/EditionsPage/NewEditionButton.tsx index 5a482add7..4174a6fa7 100644 --- a/frontend/src/components/EditionsPage/NewEditionButton.tsx +++ b/frontend/src/components/EditionsPage/NewEditionButton.tsx @@ -1,9 +1,6 @@ -import { StyledNewEditionButton } from "./styles"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus"; -import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { useAuth } from "../../contexts"; import { Role } from "../../data/enums"; +import { CreateButton } from "../Common/Buttons"; interface Props { onClick: () => void; @@ -21,8 +18,6 @@ export default function NewEditionButton({ onClick }: Props) { } return ( - - Create new edition - + ); } diff --git a/frontend/src/components/EditionsPage/styles.ts b/frontend/src/components/EditionsPage/styles.ts index adc52230a..34f30c3c5 100644 --- a/frontend/src/components/EditionsPage/styles.ts +++ b/frontend/src/components/EditionsPage/styles.ts @@ -37,3 +37,27 @@ export const StyledNewEditionButton = styled(Button).attrs(() => ({ border-color: var(--osoc_orange); } `; + +interface TextProps { + readonly: boolean; + clickable: boolean; +} + +export const StyledReadonlyText = styled.div` + text-decoration: none; + transition: 200ms ease-out; + color: ${props => (props.readonly ? "var(--osoc_red)" : "var(--osoc_green)")}; + font-weight: bold; + + // Only change style on hover for admins + ${({ clickable }) => + clickable && + ` + &:hover { + text-decoration: underline; + transition: 200ms ease-out; + color: var(--osoc_orange); + cursor: pointer; + } + `}; +`; diff --git a/frontend/src/components/Footer/FooterLinks.tsx b/frontend/src/components/Footer/FooterLinks.tsx index 127bc33a7..98f8f1441 100644 --- a/frontend/src/components/Footer/FooterLinks.tsx +++ b/frontend/src/components/Footer/FooterLinks.tsx @@ -1,6 +1,6 @@ import { Col, Container, Row } from "react-bootstrap"; import { FooterLink } from "./styles"; -import { BASE_URL } from "../../settings"; +import { BE_BASE_URL } from "../../settings"; export default function FooterLinks() { return ( @@ -8,7 +8,7 @@ export default function FooterLinks() {

Documentation

- Backend API + Backend API
{/* This link is always production because we don't host the docs locally */} Frontend diff --git a/frontend/src/components/LoginComponents/SocialButtons/SocialButtons.tsx b/frontend/src/components/LoginComponents/SocialButtons/SocialButtons.tsx index 8ce39a557..68bba61a4 100644 --- a/frontend/src/components/LoginComponents/SocialButtons/SocialButtons.tsx +++ b/frontend/src/components/LoginComponents/SocialButtons/SocialButtons.tsx @@ -1,17 +1,24 @@ -import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; -import { SocialsContainer, Socials, GoogleLoginContainer } from "./styles"; +import { GithubLoginButton } from "react-social-login-buttons"; +import { SocialsContainer, Socials } from "./styles"; +import { GITHUB_CLIENT_ID, FE_BASE_URL } from "../../../settings"; +import { generateRegisterState } from "../../../utils/session-storage"; /** - * Container for the _Sign in with Google_ and _Sign in with GitHub_ buttons. + * Container for the _Sign in with GitHub_ button. */ export default function SocialButtons() { + async function callGitHubLogIn() { + let authUrl = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}`; + authUrl += `&redirect_uri=${encodeURIComponent(`${FE_BASE_URL}/oauth/github`)}`; + authUrl += `&state=${generateRegisterState()}`; + + window.location.replace(authUrl); + } + return ( - - - - + ); diff --git a/frontend/src/components/LoginComponents/SocialButtons/styles.ts b/frontend/src/components/LoginComponents/SocialButtons/styles.ts index 8ed2a53d5..53949804f 100644 --- a/frontend/src/components/LoginComponents/SocialButtons/styles.ts +++ b/frontend/src/components/LoginComponents/SocialButtons/styles.ts @@ -10,6 +10,3 @@ export const Socials = styled.div` min-width: 230px; height: fit-content; `; -export const GoogleLoginContainer = styled.div` - margin-bottom: 15px; -`; diff --git a/frontend/src/components/LoginComponents/WelcomeText/WelcomeText.tsx b/frontend/src/components/LoginComponents/WelcomeText/WelcomeText.tsx index d19300738..ba80514ec 100644 --- a/frontend/src/components/LoginComponents/WelcomeText/WelcomeText.tsx +++ b/frontend/src/components/LoginComponents/WelcomeText/WelcomeText.tsx @@ -6,12 +6,12 @@ import { WelcomeTextContainer } from "./styles"; export default function WelcomeText() { return ( -

Hi!

-

- Welcome to the Open Summer of Code selections app. After you've logged in with your - account, we'll enable your account so you can get started. An admin will verify you - as soon as possible. -

+

Hi there!

+

Welcome to the Open Summer of Code selections app.

+
+ After you've logged in with your account, we'll enable your account so you can get + started. An admin will verify you as soon as possible. +
); } diff --git a/frontend/src/components/Navbar/EditionDropdown.tsx b/frontend/src/components/Navbar/EditionDropdown.tsx index c6e7ceb78..568eb7c2e 100644 --- a/frontend/src/components/Navbar/EditionDropdown.tsx +++ b/frontend/src/components/Navbar/EditionDropdown.tsx @@ -1,12 +1,13 @@ -import React from "react"; +import React, { useEffect } from "react"; import NavDropdown from "react-bootstrap/NavDropdown"; -import { StyledDropdownItem } from "./styles"; +import { StyledDropdownItem, DropdownLabel } from "./styles"; import { useLocation, useNavigate } from "react-router-dom"; import { getCurrentEdition, setCurrentEdition } from "../../utils/session-storage"; import { getBestRedirect } from "../../utils/logic"; +import { Edition } from "../../data/interfaces"; interface Props { - editions: string[]; + editions: Edition[]; } /** @@ -17,17 +18,25 @@ export default function EditionDropdown(props: Props) { const location = useLocation(); const navigate = useNavigate(); + useEffect(() => {}, []); + // User can't access any editions yet, no point in rendering the dropdown either // as it would just show "UNDEFINED" at the top if (props.editions.length === 0) { return null; } + // User can only access one edition, just show the label + // Don't make it a dropdown & don't make it clickable + if (props.editions.length === 1) { + return {props.editions[0].name}; + } + // If anything went wrong loading the edition, default to the first one // found in the list of editions // This shouldn't happen, but just in case // The list can never be empty because then we return null above ^ - const currentEdition = getCurrentEdition() || props.editions[0]; + const currentEdition = getCurrentEdition() || props.editions[0].name; /** * Change the route based on the edition @@ -42,17 +51,17 @@ export default function EditionDropdown(props: Props) { } // Load dropdown items dynamically - props.editions.forEach((edition: string) => { + props.editions.forEach((edition: Edition) => { navItems.push( handleSelect(edition)} + key={edition.name} + active={currentEdition === edition.name} + onClick={() => handleSelect(edition.name)} > - {edition} + {edition.name} ); }); - return {navItems}; + return {navItems}; } diff --git a/frontend/src/components/Navbar/LogoutButton.tsx b/frontend/src/components/Navbar/LogoutButton.tsx index b9945cfe1..5f2db1920 100644 --- a/frontend/src/components/Navbar/LogoutButton.tsx +++ b/frontend/src/components/Navbar/LogoutButton.tsx @@ -1,4 +1,4 @@ -import { LogOutText } from "./styles"; +import { LogOutText, LogOutTextHM } from "./styles"; import { logOut, useAuth } from "../../contexts"; import { useNavigate } from "react-router-dom"; @@ -20,5 +20,14 @@ export default function LogoutButton() { navigate("/login"); } - return Log Out; + return ( + <> + + Log Out + + + Log Out + + + ); } diff --git a/frontend/src/components/Navbar/Navbar.tsx b/frontend/src/components/Navbar/Navbar.tsx index 7d4a1dac9..2c1333834 100644 --- a/frontend/src/components/Navbar/Navbar.tsx +++ b/frontend/src/components/Navbar/Navbar.tsx @@ -1,4 +1,4 @@ -import { BSNavbar } from "./styles"; +import { BSNavbar, HorizontalSep, VerticalSep } from "./styles"; import { useAuth } from "../../contexts"; import Nav from "react-bootstrap/Nav"; import EditionDropdown from "./EditionDropdown"; @@ -11,7 +11,7 @@ import NavbarBase from "./NavbarBase"; import { LinkContainer } from "react-router-bootstrap"; import EditionNavLink from "./EditionNavLink"; import StudentsDropdown from "./StudentsDropdown"; - +import SocketDummy from "./SocketDummy"; /** * Navbar component displayed at the top of the screen. * If the user is not signed in, this is hidden automatically. @@ -43,12 +43,19 @@ export default function Navbar() { // Matched /editions/new path if (editionId === "new") { editionId = null; + } else if (editionId && !editions.find(e => e.name === editionId)) { + // If the edition was not found in the user's list of editions, + // don't display it in the navbar! + // This will lead to a 404 or 403 re-route either way, so keep + // the previous/the best edition displayed in the dropdown + editionId = null; } // If the current URL contains an edition, use that // if not (eg. /editions), check SessionStorage // otherwise, use the most-recent edition from the auth response - const currentEdition = editionId || getCurrentEdition() || editions[0]; + const currentEdition = + editionId || getCurrentEdition() || (editions[0] && editions[0].name) || ""; // Set the value of the new edition in SessionStorage if useful if (currentEdition) { @@ -57,11 +64,14 @@ export default function Navbar() { return ( + {/* Make Navbar responsive (hamburger menu) */} diff --git a/frontend/src/components/Navbar/NavbarBase.tsx b/frontend/src/components/Navbar/NavbarBase.tsx index 5c0c16680..12ce4455b 100644 --- a/frontend/src/components/Navbar/NavbarBase.tsx +++ b/frontend/src/components/Navbar/NavbarBase.tsx @@ -10,7 +10,7 @@ import Brand from "./Brand"; export default function NavbarBase({ children }: { children?: React.ReactNode }) { return ( - + {children} diff --git a/frontend/src/components/Navbar/SocketDummy.tsx b/frontend/src/components/Navbar/SocketDummy.tsx new file mode 100644 index 000000000..23f4d30b0 --- /dev/null +++ b/frontend/src/components/Navbar/SocketDummy.tsx @@ -0,0 +1,15 @@ +/** + * React doesn't allow updating the state of the SocketProvider from inside the Navbar + * so this is a hacky workaround that allows it :) + */ +import { useEffect } from "react"; +import { useSockets } from "../../contexts/socket-context"; + +export default function SocketDummy({ edition }: { edition: string }) { + const { ensureSocket } = useSockets(); + + useEffect(() => { + ensureSocket(edition); + }, [edition, ensureSocket]); + return null; +} diff --git a/frontend/src/components/Navbar/StudentsDropdown.tsx b/frontend/src/components/Navbar/StudentsDropdown.tsx index 355e1c11b..b0bb01915 100644 --- a/frontend/src/components/Navbar/StudentsDropdown.tsx +++ b/frontend/src/components/Navbar/StudentsDropdown.tsx @@ -1,10 +1,13 @@ import NavDropdown from "react-bootstrap/NavDropdown"; import { LinkContainer } from "react-router-bootstrap"; import { StyledDropdownItem } from "./styles"; +import { Role } from "../../data/enums"; +import { Nav } from "react-bootstrap"; interface Props { isLoggedIn: boolean; currentEdition: string; + role: Role | null; } /** @@ -12,16 +15,26 @@ interface Props { * @constructor */ export default function StudentsDropdown(props: Props) { - if (!props.isLoggedIn) return null; + if (!props.isLoggedIn || !props.currentEdition) return null; - return ( - + if (props.role === Role.COACH) { + return ( - Students + Students - - Email History - - - ); + ); + } else if (props.role === Role.ADMIN) { + return ( + + + Students + + + States + + + ); + } else { + return null; + } } diff --git a/frontend/src/components/Navbar/styles.ts b/frontend/src/components/Navbar/styles.ts index 304fc980b..6f011e90e 100644 --- a/frontend/src/components/Navbar/styles.ts +++ b/frontend/src/components/Navbar/styles.ts @@ -28,6 +28,7 @@ export const StyledDropdownItem = styled(NavDropdown.Item)` `; export const LogOutText = styled(BSNavbar.Text)` + padding: 8px; transition: 150ms ease-out; &:hover { @@ -36,3 +37,26 @@ export const LogOutText = styled(BSNavbar.Text)` transition: 150ms ease-in; } `; + +export const LogOutTextHM = styled(BSNavbar.Text)` + padding: 8px 0; + transition: 150ms ease-out; + + &:hover { + cursor: pointer; + color: rgba(255, 255, 255, 75%); + transition: 150ms ease-in; + } +`; + +export const HorizontalSep = styled.hr` + margin: 5px 0; +`; + +export const VerticalSep = styled.div` + margin: 0 10px; +`; + +export const DropdownLabel = styled.div` + color: rgba(255, 255, 255, 0.55); +`; diff --git a/frontend/src/components/ProjectDetailComponents/AddStudentModal/AddStudentModal.tsx b/frontend/src/components/ProjectDetailComponents/AddStudentModal/AddStudentModal.tsx new file mode 100644 index 000000000..2b102aa36 --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/AddStudentModal/AddStudentModal.tsx @@ -0,0 +1,58 @@ +import { CreateButton } from "../../Common/Buttons"; +import { FormControl } from "../../Common/Forms"; +import { StyledModal, ModalHeader, ModalFooter, Button } from "./styles"; +import FloatingLabel from "react-bootstrap/FloatingLabel"; +import { useState } from "react"; +import { AddStudentRole } from "../../../data/interfaces/projects"; + +export default function AddStudentModal({ + visible, + handleClose, + handleConfirm, + result, +}: { + visible: boolean; + handleClose: () => void; + handleConfirm: (motivation: string, addStudentRole: AddStudentRole) => void; + result: AddStudentRole; +}) { + const [motivation, setMotivation] = useState(""); + return ( + + + Suggest student for project + + + + Please motivate your decision + + { + setMotivation(e.target.value); + }} + /> + + + + + + { + handleConfirm(motivation, result); + setMotivation(""); + }} + /> + + + ); +} diff --git a/frontend/src/components/ProjectDetailComponents/AddStudentModal/index.ts b/frontend/src/components/ProjectDetailComponents/AddStudentModal/index.ts new file mode 100644 index 000000000..99017c95f --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/AddStudentModal/index.ts @@ -0,0 +1 @@ +export { default } from "./AddStudentModal"; diff --git a/frontend/src/components/ProjectDetailComponents/AddStudentModal/styles.ts b/frontend/src/components/ProjectDetailComponents/AddStudentModal/styles.ts new file mode 100644 index 000000000..a819807e2 --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/AddStudentModal/styles.ts @@ -0,0 +1,28 @@ +import styled from "styled-components"; +import Modal from "react-bootstrap/Modal"; + +export const StyledModal = styled(Modal)` + color: white; + background-color: #00000060; + margin-top: 5%; + .modal-content { + background-color: #272741; + border-radius: 5px; + border-color: var(--osoc_green); + } +`; + +export const ModalHeader = styled(Modal.Header)` + border-bottom: 1px solid #131329; +`; +export const ModalFooter = styled(Modal.Footer)` + border-top: 1px solid #131329; +`; + +export const Button = styled.button` + border-radius: 5px; + border: none; + padding: 5px 10px; + background-color: #131329; + color: white; +`; diff --git a/frontend/src/components/ProjectDetailComponents/CoachInput/CoachInput.tsx b/frontend/src/components/ProjectDetailComponents/CoachInput/CoachInput.tsx new file mode 100644 index 000000000..01a5b0af5 --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/CoachInput/CoachInput.tsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { Project } from "../../../data/interfaces"; +import { User } from "../../../utils/api/users/users"; + +import { getCoaches } from "../../../utils/api/users/coaches"; +import { Input, AddButton } from "../PartnerInput/styles"; + +export default function CoachInput({ + project, + setProject, +}: { + project: Project; + setProject: (project: Project) => void; +}) { + const [coach, setCoach] = useState(""); + const [availableCoaches, setAvailableCoaches] = useState([]); + + const params = useParams(); + const editionId = params.editionId!; + + useEffect(() => { + async function callCoaches() { + setAvailableCoaches((await getCoaches(editionId, coach, 0))?.users || []); + } + callCoaches(); + }, [coach, editionId]); + + return ( + <> + { + setCoach(e.target.value); + }} + list="coaches" + placeholder="Coach" + /> + + + {availableCoaches.map((availableCoach, _index) => { + return + + { + addToCoaches(); + }} + > + Add Coach + + + ); + + function addToCoaches() { + let coachToAdd = null; + availableCoaches.forEach(availableCoach => { + if (availableCoach.name === coach) { + coachToAdd = availableCoach; + } + }); + if (coachToAdd) { + if (!project.coaches.some(presentCoach => presentCoach.name === coach)) { + const newCoaches = [...project.coaches]; + newCoaches.push(coachToAdd); + setProject({ ...project, coaches: newCoaches }); + } + } + setCoach(""); + } +} diff --git a/frontend/src/components/ProjectDetailComponents/CoachInput/index.ts b/frontend/src/components/ProjectDetailComponents/CoachInput/index.ts new file mode 100644 index 000000000..72a50c9a9 --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/CoachInput/index.ts @@ -0,0 +1 @@ +export { default } from "./CoachInput"; diff --git a/frontend/src/components/ProjectDetailComponents/PartnerInput/PartnerInput.tsx b/frontend/src/components/ProjectDetailComponents/PartnerInput/PartnerInput.tsx new file mode 100644 index 000000000..ac833dfcd --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/PartnerInput/PartnerInput.tsx @@ -0,0 +1,43 @@ +import { useState } from "react"; +import { Project, Partner } from "../../../data/interfaces"; +import { Input, AddButton } from "./styles"; + +export default function PartnerInput({ + project, + setProject, +}: { + project: Project; + setProject: (project: Project) => void; +}) { + const [partner, setPartner] = useState(""); + + return ( + <> + { + setPartner(e.target.value); + }} + placeholder="Partner" + /> + { + addToPartners(); + }} + > + Add Partner + + + ); + + function addToPartners() { + if (!project.partners.some(presentPartner => presentPartner.name === partner)) { + const newPartner: Partner = { name: partner }; + const newPartners = [...project.partners]; + newPartners.push(newPartner); + const newProject: Project = { ...project, partners: newPartners }; + setProject(newProject); + } + setPartner(""); + } +} diff --git a/frontend/src/components/ProjectDetailComponents/PartnerInput/index.ts b/frontend/src/components/ProjectDetailComponents/PartnerInput/index.ts new file mode 100644 index 000000000..2a6beef4c --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/PartnerInput/index.ts @@ -0,0 +1 @@ +export { default } from "./PartnerInput"; diff --git a/frontend/src/components/ProjectDetailComponents/PartnerInput/styles.ts b/frontend/src/components/ProjectDetailComponents/PartnerInput/styles.ts new file mode 100644 index 000000000..d56966fa2 --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/PartnerInput/styles.ts @@ -0,0 +1,22 @@ +import styled from "styled-components"; + +export const Input = styled.input` + margin-right: 5px; + padding: 5px 10px; + background-color: #131329; + color: white; + border: none; + border-radius: 5px; + width: 15%; + min-width: 100px; +`; + +export const AddButton = styled.button` + padding: 0 10px; + background-color: #00bfff; + color: white; + border: none; + margin-right: 10px; + border-radius: 5px; + min-height: 34px; +`; diff --git a/frontend/src/components/ProjectDetailComponents/ProjectCoaches/ProjectCoaches.tsx b/frontend/src/components/ProjectDetailComponents/ProjectCoaches/ProjectCoaches.tsx new file mode 100644 index 000000000..a3e545a31 --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/ProjectCoaches/ProjectCoaches.tsx @@ -0,0 +1,43 @@ +import { TiDeleteOutline } from "react-icons/ti"; +import { CoachContainer, CoachesContainer, CoachText, RemoveButton } from "./styles"; +import CoachInput from "../CoachInput"; +import { Project } from "../../../data/interfaces"; + +export default function ProjectCoaches({ + project, + editedProject, + setEditedProject, + editing, +}: { + project: Project; + editedProject: Project; + setEditedProject: (project: Project) => void; + editing: boolean; +}) { + return ( + + {editedProject.coaches.map((element, _index) => ( + + {element.name} + {editing && ( + { + const newCoaches = [...editedProject.coaches]; + + newCoaches.splice(_index, 1); + const newProject: Project = { + ...project, + coaches: newCoaches, + }; + setEditedProject(newProject); + }} + > + + + )} + + ))} + {editing && } + + ); +} diff --git a/frontend/src/components/ProjectDetailComponents/ProjectCoaches/index.ts b/frontend/src/components/ProjectDetailComponents/ProjectCoaches/index.ts new file mode 100644 index 000000000..9c46074f5 --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/ProjectCoaches/index.ts @@ -0,0 +1 @@ +export { default } from "./ProjectCoaches"; diff --git a/frontend/src/components/ProjectDetailComponents/ProjectCoaches/styles.ts b/frontend/src/components/ProjectDetailComponents/ProjectCoaches/styles.ts new file mode 100644 index 000000000..5e071c71d --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/ProjectCoaches/styles.ts @@ -0,0 +1,44 @@ +import styled from "styled-components"; + +export const CoachesContainer = styled.div` + display: flex; + align-items: center; + margin-top: 20px; + overflow-x: hidden; + padding-bottom: 15px; + + :hover { + overflow: auto; + } +`; + +export const CoachContainer = styled.div` + background-color: #1a1a36; + border-radius: 5px; + margin-right: 10px; + text-align: center; + padding: 7.5px 15px; + width: fit-content; + max-width: 20vw; + display: flex; +`; + +export const CoachText = styled.div` + overflow: hidden; + white-space: nowrap; + + :hover { + overflow: auto; + } +`; + +export const RemoveButton = styled.button` + padding: 0 2.5px; + background-color: #f14a3b; + color: white; + border: none; + margin-left: 10px; + border-radius: 1px; + display: flex; + align-items: center; +`; diff --git a/frontend/src/components/ProjectDetailComponents/ProjectPartners/ProjectPartners.tsx b/frontend/src/components/ProjectDetailComponents/ProjectPartners/ProjectPartners.tsx new file mode 100644 index 000000000..be8f0a8fe --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/ProjectPartners/ProjectPartners.tsx @@ -0,0 +1,43 @@ +import { TiDeleteOutline } from "react-icons/ti"; +import { Project } from "../../../data/interfaces"; +import PartnerInput from "../PartnerInput"; + +import { ClientContainer, Client, RemoveButton, ClientsContainer } from "./styles"; + +export default function ProjectPartners({ + project, + editedProject, + setEditedProject, + editing, +}: { + project: Project; + editedProject: Project; + setEditedProject: (project: Project) => void; + editing: boolean; +}) { + return ( + + {editedProject.partners.map((element, _index) => ( + + {element.name} + {editing && ( + { + const newPartners = [...editedProject.partners]; + newPartners.splice(_index, 1); + const newProject: Project = { + ...project, + partners: newPartners, + }; + setEditedProject(newProject); + }} + > + + + )} + + ))} + {editing && } + + ); +} diff --git a/frontend/src/components/ProjectDetailComponents/ProjectPartners/index.ts b/frontend/src/components/ProjectDetailComponents/ProjectPartners/index.ts new file mode 100644 index 000000000..a069a1af2 --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/ProjectPartners/index.ts @@ -0,0 +1 @@ +export { default } from "./ProjectPartners"; diff --git a/frontend/src/components/ProjectDetailComponents/ProjectPartners/styles.ts b/frontend/src/components/ProjectDetailComponents/ProjectPartners/styles.ts new file mode 100644 index 000000000..581611473 --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/ProjectPartners/styles.ts @@ -0,0 +1,40 @@ +import styled from "styled-components"; + +export const ClientsContainer = styled.div` + display: flex; + align-items: center; + max-width: 85%; + overflow: hidden; + :hover { + overflow: hidden; + } +`; + +export const ClientContainer = styled.div` + display: flex; + align-items: center; + margin-right: 2%; + width: fit-content; + max-width: 20vw; +`; + +export const Client = styled.h5` + margin-bottom: 0; + margin-right: 0; + overflow: hidden; + white-space: nowrap; + :hover { + overflow: hidden; + } +`; + +export const RemoveButton = styled.button` + padding: 0 2.5px; + background-color: transparent; + color: lightgray; + border: none; + margin-left: 5px; + border-radius: 1px; + display: flex; + align-items: center; +`; diff --git a/frontend/src/components/ProjectDetailComponents/ProjectRoles/AddNewSkill/AddNewSkill.tsx b/frontend/src/components/ProjectDetailComponents/ProjectRoles/AddNewSkill/AddNewSkill.tsx new file mode 100644 index 000000000..38675243b --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/ProjectRoles/AddNewSkill/AddNewSkill.tsx @@ -0,0 +1,144 @@ +import { ChangeEvent, useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { toast } from "react-toastify"; +import { Skill } from "../../../../data/interfaces/skills"; +import { createProjectRole } from "../../../../utils/api/projectRoles"; +import { getSkills } from "../../../../utils/api/skills"; +import { CancelButton } from "../../../../views/CreateEditionPage/styles"; +import { CreateButton } from "../../../Common/Buttons"; +import { DescriptionInput } from "../../../ProjectsComponents/CreateProjectComponents/AddedSkills/styles"; +import { + AddNewSkillContainer, + NewSkill, + StyledFormSelect, + AmountInput, + AddNewSkillButton, + NewSkillLeft, + NewSKillRight, + NewSkillTop, + NewSkillBottom, +} from "./styles"; + +export default function AddNewSkill({ + setGotProject, +}: { + setGotProject: (value: boolean) => void; +}) { + const [addingSkill, setAddingSkill] = useState(false); + + const [availableSkills, setAvailableSkills] = useState([]); + + const [slots, setSlots] = useState(1); + const [chosenSkill, setChosenSkill] = useState(); + const [description, setDescription] = useState(""); + + const params = useParams(); + const editionId = params.editionId!; + const projectId = params.projectId!; + + useEffect(() => { + async function callSkills() { + setAvailableSkills((await getSkills())?.skills || []); + setChosenSkill((await getSkills())?.skills[0]); + } + callSkills(); + }, []); + + return ( + <> + {!addingSkill ? ( + + setAddingSkill(true)} /> + + ) : ( + + + + ) => { + let skillToAdd: Skill | undefined; + availableSkills.forEach(availableSkill => { + if (availableSkill.name === e.target.value) { + skillToAdd = availableSkill; + } + }); + if (skillToAdd) { + setChosenSkill(skillToAdd); + } + }} + > + {availableSkills.map(skill => ( + + ))} + + { + setSlots(event.target.valueAsNumber); + }} + /> + {slots === 1 ?
Student
:
Students
} +
+ + { + addSkillToProject(); + }} + /> + { + setAddingSkill(false); + setSlots(1); + setDescription(""); + }} + > + Cancel + + +
+ + { + setDescription(event.target.value); + }} + /> + +
+ )} + + ); + + async function addSkillToProject() { + if (chosenSkill) { + await toast.promise( + createProjectRole( + editionId, + projectId.toString(), + chosenSkill.skillId, + description, + slots + ), + { + pending: "Adding skill", + success: "Successfully added skill", + error: "Something went wrong", + } + ); + setGotProject(false); + setAddingSkill(false); + setSlots(1); + setDescription(""); + } + } +} diff --git a/frontend/src/components/ProjectDetailComponents/ProjectRoles/AddNewSkill/index.ts b/frontend/src/components/ProjectDetailComponents/ProjectRoles/AddNewSkill/index.ts new file mode 100644 index 000000000..30ffb3e79 --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/ProjectRoles/AddNewSkill/index.ts @@ -0,0 +1 @@ +export { default } from "./AddNewSkill"; diff --git a/frontend/src/components/ProjectDetailComponents/ProjectRoles/AddNewSkill/styles.ts b/frontend/src/components/ProjectDetailComponents/ProjectRoles/AddNewSkill/styles.ts new file mode 100644 index 000000000..c417a7dd0 --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/ProjectRoles/AddNewSkill/styles.ts @@ -0,0 +1,82 @@ +import styled from "styled-components"; +import { CreateButton } from "../../../Common/Buttons"; +import { FormControl } from "../../../Common/Forms"; + +export const AddNewSkillContainer = styled.div` + border: 2px solid #1a1a36; + border-radius: 5px; + margin: 10px 20px; + margin-top: 5vh; + margin-left: 0; + padding: 20px 20px 20px 20px; + background-color: #323252; + box-shadow: 5px 5px 15px #131329; + display: flex; + height: max-content; + justify-content: center; +`; + +export const NewSkill = styled.div` + border: 2px solid #1a1a36; + border-radius: 5px; + margin: 10px 20px; + margin-top: 5vh; + margin-left: 0; + padding: 20px 20px 20px 20px; + background-color: #323252; + box-shadow: 5px 5px 15px #131329; + align-items: center; + height: max-content; +`; + +export const NewSkillTop = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + overflow: auto; +`; + +export const NewSkillBottom = styled.div``; + +export const NewSkillLeft = styled.div` + display: flex; + align-items: center; + max-width: 50%; + overflow: auto; +`; +export const NewSKillRight = styled.div` + display: flex; + align-items: center; + max-width: 50%; + overflow: auto; +`; + +export const StyledFormSelect = styled(FormControl)` + background-color: var(--osoc_blue); + color: white; + border-color: transparent; + min-width: 75px; + max-width: 30%; + padding: 0.375rem 0.75rem; + border-radius: 0.25rem; + height: 2.5rem; +`; + +export const AmountInput = styled.input` + margin: 5px 10px; + padding: 0.36rem 0.75rem; + background-color: #131329; + color: white; + border: none; + border-radius: 0.25rem; + min-width: 75px; + max-width: 10%; + direction: rtl; + height: 2.5rem; +`; + +export const AddNewSkillButton = styled(CreateButton)` + margin-left: 10px; + margin-right: 10px; +`; diff --git a/frontend/src/components/ProjectDetailComponents/ProjectRoles/ProjectRoles.tsx b/frontend/src/components/ProjectDetailComponents/ProjectRoles/ProjectRoles.tsx new file mode 100644 index 000000000..0cceb4a4c --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/ProjectRoles/ProjectRoles.tsx @@ -0,0 +1,116 @@ +import { Droppable } from "react-beautiful-dnd"; +import { useParams } from "react-router-dom"; +import { toast } from "react-toastify"; +import { ProjectRole } from "../../../data/interfaces/projects"; +import { deleteProjectRole } from "../../../utils/api/projectRoles"; +import { DeleteButton } from "../../Common/Buttons"; +import { + NoStudents, + ProjectRoleContainer, + Suggestions, + TitleDeleteContainer, + NumberOfStudents, + DescriptionContainer, + DescriptionAndStudentAmount, +} from "./styles"; +import SuggestedStudent from "./SuggestedStudent"; +import AddNewSkill from "./AddNewSkill"; +import { isReadonlyEdition } from "../../../utils/logic"; +import { useAuth } from "../../../contexts"; +import { Role } from "../../../data/enums"; + +export default function ProjectRoles({ + projectRoles, + setGotProject, + role, +}: { + projectRoles: ProjectRole[]; + setGotProject: (value: boolean) => void; + role: Role; +}) { + const params = useParams(); + const projectId = params.projectId!; + const editionId = params.editionId!; + const { editions } = useAuth(); + + const isReadOnly = isReadonlyEdition(editionId, editions); + + return ( +
+ {projectRoles.map((projectRole, _index) => ( + + +

{projectRole.skill.name}

+ {role === Role.ADMIN && ( + { + await toast.promise( + deleteProjectRole( + editionId, + projectId, + projectRole.projectRoleId.toString() + ), + { + pending: "Deleting project role", + success: "Successfully deleted skill", + error: "Something went wrong", + }, + { + toastId: + "deleteProjectRole" + + projectRole.projectRoleId.toString(), + } + ); + setGotProject(false); + }} + /> + )} +
+ + +
{projectRole.description}
+
+ +
projectRole.slots + ? "red" + : projectRole.suggestions.length === projectRole.slots + ? "green" + : undefined + } + > + {projectRole.suggestions.length.toString() + + " / " + + projectRole.slots.toString()} +
+
+
+ + + {(provided, snapshot) => ( + + {projectRole.suggestions.length === 0 ? ( + Drag students here + ) : ( + projectRole.suggestions.map((sug, _index2) => ( + + )) + )} + {provided.placeholder} + + )} + +
+ ))} + {!isReadOnly && role === Role.ADMIN && } +
+ ); +} diff --git a/frontend/src/components/ProjectDetailComponents/ProjectRoles/SuggestedStudent/SuggestedStudent.tsx b/frontend/src/components/ProjectDetailComponents/ProjectRoles/SuggestedStudent/SuggestedStudent.tsx new file mode 100644 index 000000000..46f97ff1a --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/ProjectRoles/SuggestedStudent/SuggestedStudent.tsx @@ -0,0 +1,104 @@ +import { Draggable } from "react-beautiful-dnd"; +import { useNavigate, useParams } from "react-router-dom"; +import { toast } from "react-toastify"; +import { useAuth } from "../../../../contexts"; +import { Role } from "../../../../data/enums"; +import { ProjectRole, ProjectRoleSuggestion } from "../../../../data/interfaces/projects"; +import { deleteStudentFromProject } from "../../../../utils/api/projectStudents"; +import { DeleteButton } from "../../../Common/Buttons"; +import { DrafterContainer, NameDeleteContainer, SuggestionContainer, StudentName } from "./styles"; + +export default function SuggestedStudent({ + suggestion, + projectRole, + index, + setGotProject, + notDraggable, +}: { + suggestion: ProjectRoleSuggestion; + projectRole: ProjectRole; + index: number; + setGotProject: (value: boolean) => void; + notDraggable: boolean; +}) { + const params = useParams(); + const projectId = parseInt(params.projectId!); + const editionId = params.editionId!; + + const { role, userId } = useAuth(); + + const navigate = useNavigate(); + const studentRemoved = suggestion.student === null; + + return ( + + {(provided, snapshot) => ( + + + {} + : () => + navigate( + "/editions/" + + editionId + + "/students/" + + suggestion.student.studentId + ) + } + > + {studentRemoved + ? "[STUDENT REMOVED]" + : suggestion.student.firstName + " " + suggestion.student.lastName} + + {(role === Role.ADMIN || userId === suggestion.drafter.userId) && + !studentRemoved && ( + { + await toast.promise( + deleteStudentFromProject( + editionId, + projectId.toString(), + projectRole.projectRoleId.toString(), + suggestion.student.studentId.toString() + ), + { + pending: "Deleting student from project", + success: "Successfully removed student", + error: "Something went wrong", + } + ); + setGotProject(false); + }} + /> + )} + + + {suggestion.drafter && suggestion.argumentation !== "" ? ( + <> + By {suggestion.drafter.name}:{" " + suggestion.argumentation} + + ) : ( + suggestion.drafter && <>By {suggestion.drafter.name} + )} + + + )} + + ); +} diff --git a/frontend/src/components/ProjectDetailComponents/ProjectRoles/SuggestedStudent/index.ts b/frontend/src/components/ProjectDetailComponents/ProjectRoles/SuggestedStudent/index.ts new file mode 100644 index 000000000..e65b7e3c7 --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/ProjectRoles/SuggestedStudent/index.ts @@ -0,0 +1 @@ +export { default } from "./SuggestedStudent"; diff --git a/frontend/src/components/ProjectDetailComponents/ProjectRoles/SuggestedStudent/styles.ts b/frontend/src/components/ProjectDetailComponents/ProjectRoles/SuggestedStudent/styles.ts new file mode 100644 index 000000000..6dbc9ca1d --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/ProjectRoles/SuggestedStudent/styles.ts @@ -0,0 +1,53 @@ +import styled from "styled-components"; +import { HoverAnimation } from "../../../Common/styles"; + +export const SuggestionContainer = styled.div` + background-color: #1a1a36; + border-radius: 5px; + margin-top: 10px; + margin-right: 10px; + text-align: center; + padding: 7.5px 15px; + max-width: 40vw; +`; + +export const NameDeleteContainer = styled.div` + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + margin: 5px 0; +`; + +export const DrafterContainer = styled.div` + border-radius: 5px; + padding: 15px; + margin-right: auto; + background-color: #0f0f30; + display: flex; + margin-bottom: 5px; + width: 100%; + overflow: auto; + text-align: left; + text-overflow: ellipsis; +`; + +interface StudentNameProps { + removed: boolean; +} + +export const StudentName = styled.div` + ${HoverAnimation} + overflow: auto; + text-overflow: ellipsis; + max-width: 80%; + + ${props => + !props.removed && + ` text-decoration: underline; + + &:hover { + cursor: pointer; + color: var(--osoc_green); + }`} +`; diff --git a/frontend/src/components/ProjectDetailComponents/ProjectRoles/index.ts b/frontend/src/components/ProjectDetailComponents/ProjectRoles/index.ts new file mode 100644 index 000000000..51d015fa8 --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/ProjectRoles/index.ts @@ -0,0 +1 @@ +export { default } from "./ProjectRoles"; diff --git a/frontend/src/components/ProjectDetailComponents/ProjectRoles/styles.ts b/frontend/src/components/ProjectDetailComponents/ProjectRoles/styles.ts new file mode 100644 index 000000000..09f6f83a5 --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/ProjectRoles/styles.ts @@ -0,0 +1,58 @@ +import styled from "styled-components"; + +export const ProjectRoleContainer = styled.div` + border: 2px solid #1a1a36; + border-radius: 5px; + margin: 10px 20px; + margin-left: 0; + padding: 20px 20px 20px 20px; + background-color: #323252; + box-shadow: 5px 5px 15px #131329; +`; + +export const TitleDeleteContainer = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + +export const Suggestions = styled.div` + min-height: 10vh; +`; + +export const NoStudents = styled.div` + display: flex; + align-items: center; + border: dashed #0f0f30; + border-radius: 20px; + margin: 15px 0; + padding: 20px; + min-height: 10vh; + max-width: 40vw; +`; + +export const DescriptionAndStudentAmount = styled.div` + margin-top: 5px; + display: flex; + align-items: center; + justify-content: space-between; +`; + +export const NumberOfStudents = styled.div` + .red { + color: var(--osoc_red); + } + .green { + color: var(--osoc_green); + } +`; + +export const DescriptionContainer = styled.div` + overflow: hidden; + text-overflow: ellipsis; + max-height: 2.6rem; + max-width: 90%; + :hover { + overflow: auto; + } +`; diff --git a/frontend/src/components/ProjectDetailComponents/TitleAndEdit/TitleAndEdit.tsx b/frontend/src/components/ProjectDetailComponents/TitleAndEdit/TitleAndEdit.tsx new file mode 100644 index 000000000..0437584e3 --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/TitleAndEdit/TitleAndEdit.tsx @@ -0,0 +1,72 @@ +import { Title, TitleContainer, Cancel, TitleInput, Edit, EditDeleteContainer } from "./styles"; + +import { MdOutlineEditNote } from "react-icons/md"; +import { Role } from "../../../data/enums/role"; +import { Project } from "../../../data/interfaces/projects"; +import { CreateButton, DeleteButton } from "../../Common/Buttons"; + +export default function TitleAndEdit({ + editing, + project, + editedProject, + setEditedProject, + setEditing, + editProject, + role, + handleShow, +}: { + editing: boolean; + project: Project; + editedProject: Project; + setEditedProject: (project: Project) => void; + setEditing: (editing: boolean) => void; + editProject: () => Promise; + role: Role; + handleShow: () => void; +}) { + return ( + + {!editing ? ( + {project.name} + ) : ( + { + const newProject: Project = { ...project, name: e.target.value }; + setEditedProject(newProject); + }} + /> + )} + {role === Role.ADMIN && ( + + {!editing ? ( + + setEditing(true)} /> + + ) : ( + <> + { + await editProject(); + setEditing(false); + }} + /> + { + setEditing(false); + setEditedProject(project); + }} + > + Cancel + + + )} + + + )} + + ); +} diff --git a/frontend/src/components/ProjectDetailComponents/TitleAndEdit/index.ts b/frontend/src/components/ProjectDetailComponents/TitleAndEdit/index.ts new file mode 100644 index 000000000..e3c7eb47b --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/TitleAndEdit/index.ts @@ -0,0 +1 @@ +export { default } from "./TitleAndEdit"; diff --git a/frontend/src/components/ProjectDetailComponents/TitleAndEdit/styles.ts b/frontend/src/components/ProjectDetailComponents/TitleAndEdit/styles.ts new file mode 100644 index 000000000..d5849cfaf --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/TitleAndEdit/styles.ts @@ -0,0 +1,49 @@ +import styled from "styled-components"; + +export const TitleContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + margin-right: 20px; +`; + +export const Title = styled.h2` + overflow: hidden; + margin-right: 10px; + max-height: 3.6em; + line-height: 1.3em; + :hover { + overflow: auto; + } +`; + +export const TitleInput = styled.input` + padding: 5px 10px; + background-color: #131329; + color: white; + border: none; + border-radius: 5px; +`; + +export const EditDeleteContainer = styled.div` + display: flex; + align-items: center; +`; + +export const Edit = styled.div` + :hover { + cursor: pointer; + } +`; + +export const Cancel = styled.button` + padding: 5px 10px; + background-color: #131329; + color: white; + border: none; + margin-left: 5px; + margin-right: 5px; + border-radius: 5px; + height: 38px; +`; diff --git a/frontend/src/components/ProjectDetailComponents/index.ts b/frontend/src/components/ProjectDetailComponents/index.ts new file mode 100644 index 000000000..78443d050 --- /dev/null +++ b/frontend/src/components/ProjectDetailComponents/index.ts @@ -0,0 +1,7 @@ +export { default as TitleAndEdit } from "./TitleAndEdit"; +export { default as PartnerInput } from "./PartnerInput"; +export { default as CoachInput } from "./CoachInput"; +export { default as ProjectRoles } from "./ProjectRoles"; +export { default as ProjectCoaches } from "./ProjectCoaches"; +export { default as ProjectPartners } from "./ProjectPartners"; +export { default as AddStudentModal } from "./AddStudentModal"; diff --git a/frontend/src/components/ProjectsComponents/Conflicts/Conflict.tsx b/frontend/src/components/ProjectsComponents/Conflicts/Conflict.tsx new file mode 100644 index 000000000..6e6b10455 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/Conflicts/Conflict.tsx @@ -0,0 +1,27 @@ +import { Conflict } from "../../../data/interfaces"; +import { ListLink } from "./styles"; + +/** + * A list-item which contains a student and all his assigned projects + */ +export default function ConflictDiv(props: { editionId: string; conflict: Conflict }) { + return ( +
  • + + {`${props.conflict.firstName} ${props.conflict.lastName}`} + +
      + {props.conflict.prSuggestions.map(prSuggestion => ( +
    • + + {prSuggestion.projectRole.project.name} + +
    • + ))} +
    +
    +
  • + ); +} diff --git a/frontend/src/components/ProjectsComponents/Conflicts/ConflictsButton.tsx b/frontend/src/components/ProjectsComponents/Conflicts/ConflictsButton.tsx new file mode 100644 index 000000000..54cfdc24d --- /dev/null +++ b/frontend/src/components/ProjectsComponents/Conflicts/ConflictsButton.tsx @@ -0,0 +1,130 @@ +import { ConflictButtonDiv, SidePanel } from "./styles"; +import { useEffect, useState } from "react"; +import { Offcanvas } from "react-bootstrap"; +import { getConflicts } from "../../../utils/api/conflicts"; +import { Conflict } from "../../../data/interfaces"; +import ConflictDiv from "./Conflict"; +import LoadSpinner from "../../Common/LoadSpinner"; +import CreateButton from "../../Common/Buttons/CreateButton"; +import WarningButton from "../../Common/Buttons/WarningButton"; +import { EventType, RequestMethod, WebSocketEvent } from "../../../data/interfaces/websockets"; +import { useSockets } from "../../../contexts"; + +const wsEventTypes = [EventType.PROJECT, EventType.PROJECT_ROLE, EventType.PROJECT_ROLE_SUGGESTION]; + +/** + * A button which opens a side-panel to show all students who are assigned to multiple projects. + */ +export default function ConflictsButton(props: { editionId: string }) { + const { socket } = useSockets(); + + const [conflicts, setConflicts] = useState(undefined); + const [show, setShow] = useState(false); + + const handleClose = () => setShow(false); + const handleShow = () => setShow(true); + + useEffect(() => { + const fetchConflicts = async () => { + const conflicts = await getConflicts(props.editionId); + setConflicts(conflicts.conflictStudents); + }; + + fetchConflicts().catch(console.error); + }, [props.editionId]); + + useEffect(() => { + function listener(event: MessageEvent) { + const data = JSON.parse(event.data) as WebSocketEvent; + if (!wsEventTypes.includes(data.eventType)) return; + + // Re-fetch the conflicts if: + // - Project deleted + // - Role deleted + // - Suggestion added/changed/deleted + const containerDeleted = + (data.eventType === EventType.PROJECT || + data.eventType === EventType.PROJECT_ROLE) && + data.method === RequestMethod.DELETE; + const suggestionChanged = data.eventType === EventType.PROJECT_ROLE_SUGGESTION; + + if (containerDeleted || suggestionChanged) { + getConflicts(props.editionId).then(conflicts => + setConflicts(conflicts.conflictStudents) + ); + } + } + + socket?.addEventListener("message", listener); + + function removeListener() { + if (socket) { + socket.removeEventListener("message", listener); + } + } + + return removeListener; + }, [props.editionId, socket]); + + if (conflicts === undefined) { + return ( + + + + ); + } + if (show) { + return ( + + + +

    Resolve Conflicts

    +
    +
    + + The student may be a better fit for a specific team, if they: +
      +
    • are an alumni and the team doesn't have any yet
    • +
    • are an alumni on a team with a half-time coach
    • +
    • are an alumni and provide skills the coach does not have
    • +
    • have pre-existing history with the project in question
    • +
    • enrich the team's diversity
    • +
    • + have a skillset that is tough to find in other applicants, +
      + and matches exceptionally well with the project +
    • +
    +

    Conflicts

    +
      + {conflicts.map(conflict => ( + + ))} +
    +
    +
    + ); + } + + if (conflicts.length === 0) { + return ( + + + No conflicts + + + ); + } + return ( + + {`Conflicts (${conflicts.length})`} + + ); +} diff --git a/frontend/src/components/ProjectsComponents/Conflicts/index.ts b/frontend/src/components/ProjectsComponents/Conflicts/index.ts new file mode 100644 index 000000000..555820fd1 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/Conflicts/index.ts @@ -0,0 +1 @@ +export { default } from "./ConflictsButton"; diff --git a/frontend/src/components/ProjectsComponents/Conflicts/styles.ts b/frontend/src/components/ProjectsComponents/Conflicts/styles.ts new file mode 100644 index 000000000..be80b3185 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/Conflicts/styles.ts @@ -0,0 +1,27 @@ +import styled from "styled-components"; +import { Offcanvas } from "react-bootstrap"; +import { Link } from "react-router-dom"; +import { HoverAnimation } from "../../Common/styles"; + +export const SidePanel = styled(Offcanvas)` + background-color: #323252; + width: 33em; + max-width: fit-content; + color: white; +`; + +export const ListLink = styled(Link)` + ${HoverAnimation}; + + color: white; + margin-right: 10px; + white-space: nowrap; + + &:hover { + color: var(--osoc_green); + } +`; + +export const ConflictButtonDiv = styled.div` + float: right; +`; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx index 6417e7964..71f8d7249 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx @@ -1,6 +1,7 @@ import { TiDeleteOutline } from "react-icons/ti"; import { User } from "../../../../utils/api/users/users"; -import { AddedItem, ItemName, RemoveButton } from "../styles"; +import { DeleteButton } from "../../../Common/Buttons"; +import { AddedItem, ItemName } from "../styles"; export default function AddedCoaches({ coaches, @@ -14,15 +15,16 @@ export default function AddedCoaches({ {coaches.map((element, _index) => ( {element.name} - { const newItems = [...coaches]; newItems.splice(_index, 1); setCoaches(newItems); }} > - - + +
    ))} diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedPartners/AddedPartners.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedPartners/AddedPartners.tsx index 0cf5b0040..8d32420d1 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedPartners/AddedPartners.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedPartners/AddedPartners.tsx @@ -1,5 +1,6 @@ import { TiDeleteOutline } from "react-icons/ti"; -import { AddedItem, ItemName, RemoveButton } from "../styles"; +import { DeleteButton } from "../../../Common/Buttons"; +import { AddedItem, ItemName } from "../styles"; export default function AddedPartners({ items, @@ -13,15 +14,16 @@ export default function AddedPartners({ {items.map((element, _index) => ( {element} - { const newItems = [...items]; newItems.splice(_index, 1); setItems(newItems); }} > - - + + ))} diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills.tsx index 1194c5c87..bd53e8139 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills.tsx @@ -1,15 +1,16 @@ import { SkillProject } from "../../../../data/interfaces/projects"; -import { Input } from "../styles"; import { AmountInput, SkillContainer, DescriptionContainer, - Delete, TopContainer, SkillName, + TopLeftContainer, + DescriptionInput, } from "./styles"; import { TiDeleteOutline } from "react-icons/ti"; import React from "react"; +import { DeleteButton } from "../../../Common/Buttons"; /** * @@ -38,10 +39,11 @@ export default function AddedSkills({ ) { const newList = skills.map((item, otherIndex) => { if (index === otherIndex) { - if (amount && !isNaN(event.target.valueAsNumber)) { + if (amount) { + if (event.target.valueAsNumber < 1) return item; return { ...item, - amount: event.target.valueAsNumber, + slots: event.target.valueAsNumber, }; } return { @@ -59,30 +61,34 @@ export default function AddedSkills({ {skills.map((skill, index) => ( - {skill.skill} + + {skill.skill.name} - { - updateSkills(event, index, true); - }} - /> - { + updateSkills(event, index, true); + }} + /> + {skill.slots === 1 ?
    student
    :
    students
    } +
    + { const newSkills = [...skills]; newSkills.splice(index, 1); setSkills(newSkills); }} > - - + +
    - { async function callCoaches() { - setAvailableCoaches((await getCoaches(editionId, coach, 0)).users); + setAvailableCoaches((await getCoaches(editionId, coach, 0))!.users); } callCoaches(); }, [coach, editionId]); @@ -35,8 +35,11 @@ export default function Coach({ onChange={e => { setCoach(e.target.value); }} + onKeyDown={e => { + if (e.key === "Enter") addCoach(); + }} list="users" - placeholder="Coach" + placeholder="Ex. Michael Scott" /> {availableCoaches.map((availableCoach, _index) => { @@ -44,32 +47,30 @@ export default function Coach({ })} - { - let coachToAdd = null; - availableCoaches.forEach(availableCoach => { - if (availableCoach.name === coach) { - coachToAdd = availableCoach; - } - }); - if (coachToAdd) { - if (!coaches.some(presentCoach => presentCoach.name === coach)) { - const newCoaches = [...coaches]; - newCoaches.push(coachToAdd); - setCoaches(newCoaches); - setShowAlert(false); - } - } else setShowAlert(true); - setCoach(""); - }} - > - Add coach - + Add - + ); + + function addCoach() { + let coachToAdd = null; + availableCoaches.forEach(availableCoach => { + if (availableCoach.name === coach) { + coachToAdd = availableCoach; + } + }); + if (coachToAdd) { + if (!coaches.some(presentCoach => presentCoach.name === coach)) { + const newCoaches = [...coaches]; + newCoaches.push(coachToAdd); + setCoaches(newCoaches); + setShowAlert(false); + } + } else setShowAlert(true); + setCoach(""); + } } function BadCoachAlert({ show, setShow }: { show: boolean; setShow: (state: boolean) => void }) { diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/InfoUrl/InfoUrl.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/InfoUrl/InfoUrl.tsx new file mode 100644 index 000000000..24fbe0d97 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/InfoUrl/InfoUrl.tsx @@ -0,0 +1,17 @@ +import { Input } from "../../styles"; + +export default function InfoUrl({ + infoUrl, + setInfoUrl, +}: { + infoUrl: string; + setInfoUrl: (infoUrl: string) => void; +}) { + return ( + setInfoUrl(e.target.value)} + placeholder="Ex. https://osoc.be/" + /> + ); +} diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/InfoUrl/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/InfoUrl/index.ts new file mode 100644 index 000000000..071478423 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/InfoUrl/index.ts @@ -0,0 +1 @@ +export { default } from "./InfoUrl"; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Name/Name.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Name/Name.tsx index 67ad8f7d9..070d4b752 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Name/Name.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Name/Name.tsx @@ -2,6 +2,10 @@ import { Input } from "../../styles"; export default function Name({ name, setName }: { name: string; setName: (name: string) => void }) { return ( - setName(e.target.value)} placeholder="Project name" /> + setName(e.target.value)} + placeholder="Ex. UGent project" + /> ); } diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx deleted file mode 100644 index 7586d250b..000000000 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Input } from "../../styles"; - -export default function NumberOfStudents({ - numberOfStudents, - setNumberOfStudents, -}: { - numberOfStudents: number; - setNumberOfStudents: (numberOfStudents: number) => void; -}) { - return ( -
    - { - setNumberOfStudents(e.target.valueAsNumber); - }} - placeholder="Number of students" - /> -
    - ); -} diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/index.ts deleted file mode 100644 index 7594e8ecf..000000000 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./NumberOfStudents"; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx index a6096de81..ecb044373 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx @@ -11,15 +11,18 @@ export default function Partner({ partners: string[]; setPartners: (partners: string[]) => void; }) { - const availablePartners = ["partner1", "partner2"]; // TODO get partners from API call + const availablePartners: string[] = []; return (
    setPartner(e.target.value)} + onKeyDown={e => { + if (e.key === "Enter") addPartner(); + }} list="partners" - placeholder="Partner" + placeholder="Ex. Open Knowledge Belgium" /> @@ -28,18 +31,16 @@ export default function Partner({ })} - { - if (!partners.includes(partner) && partner.length > 0) { - const newPartners = [...partners]; - newPartners.push(partner); - setPartners(newPartners); - } - setPartner(""); - }} - > - Add partner - + Add
    ); + + function addPartner() { + if (!partners.includes(partner) && partner.length > 0) { + const newPartners = [...partners]; + newPartners.push(partner); + setPartners(newPartners); + } + setPartner(""); + } } diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx index 588e846d4..a6cbf759c 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx @@ -1,7 +1,10 @@ +import { useEffect, useState } from "react"; import { SkillProject } from "../../../../../data/interfaces/projects"; +import { Skill } from "../../../../../data/interfaces/skills"; +import { getSkills } from "../../../../../utils/api/skills"; import { Input, AddButton } from "../../styles"; -export default function Skill({ +export default function SkillInput({ skill, setSkill, skills, @@ -12,39 +15,53 @@ export default function Skill({ skills: SkillProject[]; setSkills: (skills: SkillProject[]) => void; }) { - const availableSkills = ["Frontend", "Backend", "Database", "Design"]; + const [availableSkills, setAvailableSkills] = useState([]); + + useEffect(() => { + async function callSkills() { + setAvailableSkills((await getSkills())?.skills || []); + } + callSkills(); + }, []); return (
    setSkill(e.target.value)} - placeholder="Skill" + onKeyDown={e => { + if (e.key === "Enter") addSkill(); + }} + placeholder="Ex. Front-end Developer" list="skills" /> - {availableSkills.map((availableCoach, _index) => { - return - { - if (availableSkills.some(availableSkill => availableSkill === skill)) { - const newSkills = [...skills]; - const newSkill: SkillProject = { - skill: skill, - description: "", - amount: 1, - }; - newSkills.push(newSkill); - setSkills(newSkills); - } - setSkill(""); - }} - > - Add skill - + Add
    ); + + function addSkill() { + let skillToAdd: Skill | undefined; + availableSkills.forEach(availableSkill => { + if (availableSkill.name === skill) { + skillToAdd = availableSkill; + } + }); + if (skillToAdd) { + const newSkills = [...skills]; + const newSkill: SkillProject = { + skill: skillToAdd, + description: "", + slots: 1, + }; + newSkills.push(newSkill); + setSkills(newSkills); + } + setSkill(""); + } } diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts index b0235977d..a6cd81854 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts @@ -1,5 +1,4 @@ export { default as NameInput } from "./Name"; -export { default as NumberOfStudentsInput } from "./NumberOfStudents"; export { default as CoachInput } from "./Coach"; export { default as SkillInput } from "./Skill"; export { default as PartnerInput } from "./Partner"; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts index 63f743cf3..ea14583f2 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts @@ -1,10 +1,4 @@ -export { - NameInput, - NumberOfStudentsInput, - CoachInput, - SkillInput, - PartnerInput, -} from "./InputFields"; +export { NameInput, CoachInput, SkillInput, PartnerInput } from "./InputFields"; export { default as AddedPartners } from "./AddedPartners"; export { default as AddedCoaches } from "./AddedCoaches"; export { default as AddedSkills } from "./AddedSkills"; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts index d40fefa38..2076396ca 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts @@ -7,6 +7,8 @@ export const Input = styled.input` color: white; border: none; border-radius: 5px; + width: 35vw; + min-width: 300px; `; export const AddButton = styled.button` @@ -18,20 +20,13 @@ export const AddButton = styled.button` border-radius: 5px; `; -export const RemoveButton = styled.button` - padding: 0px 2.5px; - background-color: #f14a3b; - color: white; - border: none; - margin-left: 10px; - border-radius: 1px; - display: flex; - align-items: center; -`; - export const ItemName = styled.div` + height: auto; overflow-x: auto; text-overflow: ellipsis; + margin: auto; + margin-left: 5px; + margin-right: 5px; `; export const AddedItem = styled.div` @@ -39,10 +34,12 @@ export const AddedItem = styled.div` margin-left: 0; padding: 5px; background-color: #1a1a36; - width: fit-content; - max-width: 75%; + width: 35vw; + max-width: 50%; border-radius: 5px; display: flex; + justify-content: space-between; + align-items: center; `; export const WarningContainer = styled.div` diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index ca815f2ee..2b465b638 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -4,7 +4,6 @@ import { CoachContainer, CoachText, NumberOfStudents, - Delete, TitleContainer, Title, OpenIcon, @@ -14,7 +13,6 @@ import { } from "./styles"; import { BsPersonFill } from "react-icons/bs"; -import { HiOutlineTrash } from "react-icons/hi"; import { useState } from "react"; @@ -25,38 +23,44 @@ import { useNavigate, useParams } from "react-router-dom"; import { Project } from "../../../data/interfaces"; import { useAuth } from "../../../contexts"; import { Role } from "../../../data/enums"; +import { DeleteButton } from "../../Common/Buttons"; +import { toast } from "react-toastify"; /** * * @param project a Project object - * @param refreshProjects what to do when a project is deleted. * @returns a project card which is a small overview of a project. */ -export default function ProjectCard({ - project, - refreshProjects, -}: { - project: Project; - refreshProjects: () => void; -}) { +export default function ProjectCard({ project }: { project: Project }) { // Used for the confirm screen. const [show, setShow] = useState(false); const handleClose = () => setShow(false); const handleShow = () => setShow(true); - // What to do when deleting a project. - const handleDelete = () => { - deleteProject(project.editionName, project.projectId); - setShow(false); - refreshProjects(); - }; - - const navigate = useNavigate(); const params = useParams(); const editionId = params.editionId!; - const { role } = useAuth(); + const navigate = useNavigate(); + + let assignedStudents = 0; + let neededStudents = 0; + project.projectRoles.forEach(projectRole => { + neededStudents += projectRole.slots; + assignedStudents += projectRole.suggestions.length; + }); + + // What to do when deleting a project. + async function handleDelete() { + const success = await deleteProject(editionId, project.projectId); + setShow(false); + if (!success) { + toast.error("Could not delete project", { toastId: "deleteProject" }); + } else { + toast.success("Deleted project", { toastId: "deletedProject" }); + } + } + return ( @@ -69,18 +73,14 @@ export default function ProjectCard({ - {role === Role.ADMIN && ( - - - - )} + {role === Role.ADMIN && } + /> @@ -90,7 +90,7 @@ export default function ProjectCard({ ))} - {project.numberOfStudents} + {assignedStudents + " / " + neededStudents} diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts index d051fba64..45a86cd9b 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts +++ b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts @@ -1,6 +1,7 @@ import { Modal } from "react-bootstrap"; import styled from "styled-components"; import { BsArrowUpRightSquare } from "react-icons/bs"; +import { HoverAnimation } from "../../Common/styles"; export const CardContainer = styled.div` border: 2px solid #1a1a36; @@ -13,17 +14,22 @@ export const CardContainer = styled.div` export const TitleContainer = styled.div` display: flex; - align-items: baseline; + align-items: center; justify-content: space-between; `; export const Title = styled.h2` - text-overflow: ellipsis; + ${HoverAnimation} + max-height: 3.7em; overflow: hidden; display: flex; align-items: center; - :hover { + margin-right: 10px; + + &:hover { + overflow: hidden; cursor: pointer; + color: var(--osoc_green); } `; @@ -35,14 +41,18 @@ export const OpenIcon = styled(BsArrowUpRightSquare)` export const ClientContainer = styled.div` display: flex; - align-items: top; + align-items: center; justify-content: space-between; color: lightgray; `; export const Clients = styled.div` display: flex; - overflow-x: auto; + overflow: hidden; + max-height: 3rem; + :hover { + overflow: hidden; + } `; export const Client = styled.h5` @@ -58,8 +68,13 @@ export const NumberOfStudents = styled.div` export const CoachesContainer = styled.div` display: flex; + align-items: center; margin-top: 20px; - overflow-x: auto; + overflow-x: hidden; + padding-bottom: 15px; + :hover { + overflow: auto; + } `; export const CoachContainer = styled.div` @@ -70,23 +85,15 @@ export const CoachContainer = styled.div` padding: 7.5px 15px; width: fit-content; max-width: 20vw; + display: flex; `; export const CoachText = styled.div` overflow: hidden; - text-overflow: ellipsis; white-space: nowrap; -`; - -export const Delete = styled.button` - background-color: #f14a3b; - padding: 5px 5px; - border: 0; - border-radius: 1px; - max-height: 30px; - margin-left: 5%; - display: flex; - align-items: center; + :hover { + overflow: auto; + } `; export const PopUp = styled(Modal)``; diff --git a/frontend/src/components/ProjectsComponents/ProjectTable.tsx b/frontend/src/components/ProjectsComponents/ProjectTable.tsx new file mode 100644 index 000000000..f795903e8 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/ProjectTable.tsx @@ -0,0 +1,50 @@ +import { CardsGrid, ProjectsContainer } from "../../views/projectViews/ProjectsPage/styles"; +import { ProjectCard } from "./index"; +import InfiniteScroll from "react-infinite-scroller"; +import { Project } from "../../data/interfaces"; +import { MessageDiv } from "./styles"; +import LoadSpinner from "../Common/LoadSpinner"; + +/** + * A table of [[ProjectCard]]s. + * @param props.projects A list of projects which needs to be shown. + * @param props.loading Data is not available yet. + * @param props.getMoreProjects A function to load more projects. + * @param props.moreProjectsAvailable More unfetched projects available. + */ +export default function ProjectTable(props: { + projects: Project[]; + loading: boolean; + getMoreProjects: (page: number, reset: boolean) => void; + moreProjectsAvailable: boolean; +}) { + if (props.projects.length === 0) { + if (props.loading) { + return ; + } + return ( + +
    No projects found.
    +
    + ); + } + + return ( + props.getMoreProjects(page, false)} + hasMore={props.moreProjectsAvailable} + loader={} + initialLoad={true} + useWindow={false} + getScrollParent={() => document.getElementById("root")} + > + + + {props.projects.map((project, _index) => ( + + ))} + + + + ); +} diff --git a/frontend/src/components/ProjectsComponents/StudentPlaceholder/StudentPlaceholder.tsx b/frontend/src/components/ProjectsComponents/StudentPlaceholder/StudentPlaceholder.tsx deleted file mode 100644 index ea2c5bf33..000000000 --- a/frontend/src/components/ProjectsComponents/StudentPlaceholder/StudentPlaceholder.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { TiDeleteOutline } from "react-icons/ti"; -import { StudentPlace } from "../../../data/interfaces/projects"; -import { StudentPlaceContainer, AddStudent } from "./styles"; - -/** - * TODO this needs more work and is still mostly a placeholder. - * @param studentPlace gives some info about a specific place in a project. - * @returns a component to add a student to a project place or to view a student added to the project. - */ -export default function StudentPlaceholder({ studentPlace }: { studentPlace: StudentPlace }) { - if (studentPlace.available) { - return ( - - {studentPlace.skill} - - - ); - } else - return ( - - {studentPlace.skill} - {" " + studentPlace.name} - - - ); -} diff --git a/frontend/src/components/ProjectsComponents/StudentPlaceholder/index.ts b/frontend/src/components/ProjectsComponents/StudentPlaceholder/index.ts deleted file mode 100644 index 628878c9a..000000000 --- a/frontend/src/components/ProjectsComponents/StudentPlaceholder/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./StudentPlaceholder"; diff --git a/frontend/src/components/ProjectsComponents/StudentPlaceholder/styles.ts b/frontend/src/components/ProjectsComponents/StudentPlaceholder/styles.ts deleted file mode 100644 index e233461c3..000000000 --- a/frontend/src/components/ProjectsComponents/StudentPlaceholder/styles.ts +++ /dev/null @@ -1,17 +0,0 @@ -import styled from "styled-components"; - -import { AiOutlineUserAdd } from "react-icons/ai"; - -export const StudentPlaceContainer = styled.div` - margin-top: 30px; - padding: 20px; - background-color: #323252; - border-radius: 5px; - max-width: 50%; - display: flex; - align-items: center; -`; - -export const AddStudent = styled(AiOutlineUserAdd)` - margin-left: 10px; -`; diff --git a/frontend/src/components/ProjectsComponents/index.ts b/frontend/src/components/ProjectsComponents/index.ts index a6c527b3a..84cfb1782 100644 --- a/frontend/src/components/ProjectsComponents/index.ts +++ b/frontend/src/components/ProjectsComponents/index.ts @@ -1,3 +1,4 @@ export { default as ProjectCard } from "./ProjectCard"; -export { default as StudentPlaceholder } from "./StudentPlaceholder"; export { default as LoadSpinner } from "./LoadSpinner"; +export { default as Conflicts } from "./Conflicts"; +export { default } from "./ProjectTable"; diff --git a/frontend/src/components/ProjectsComponents/styles.ts b/frontend/src/components/ProjectsComponents/styles.ts new file mode 100644 index 000000000..14596d668 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/styles.ts @@ -0,0 +1,6 @@ +import styled from "styled-components"; + +export const MessageDiv = styled.div` + margin-top: 20px; + margin-left: 20px; +`; diff --git a/frontend/src/components/RegisterComponents/AlreadyRegistered/AlreadyRegistered.tsx b/frontend/src/components/RegisterComponents/AlreadyRegistered/AlreadyRegistered.tsx new file mode 100644 index 000000000..2aab766e5 --- /dev/null +++ b/frontend/src/components/RegisterComponents/AlreadyRegistered/AlreadyRegistered.tsx @@ -0,0 +1,13 @@ +/** + * Message shown when the user already has an account + */ +export default function AlreadyRegistered() { + return ( + <> +

    You look familiar!

    +
    + You've already got an OSOC-account. If you'd like to get added to a new edition, you + should contact an admin. + + ); +} diff --git a/frontend/src/components/RegisterComponents/AlreadyRegistered/index.ts b/frontend/src/components/RegisterComponents/AlreadyRegistered/index.ts new file mode 100644 index 000000000..8d729bf48 --- /dev/null +++ b/frontend/src/components/RegisterComponents/AlreadyRegistered/index.ts @@ -0,0 +1 @@ +export { default } from "./AlreadyRegistered"; diff --git a/frontend/src/components/RegisterComponents/SocialButtons/SocialButtons.tsx b/frontend/src/components/RegisterComponents/SocialButtons/SocialButtons.tsx index 6d19ea288..cb9e3e7ee 100644 --- a/frontend/src/components/RegisterComponents/SocialButtons/SocialButtons.tsx +++ b/frontend/src/components/RegisterComponents/SocialButtons/SocialButtons.tsx @@ -1,12 +1,26 @@ -import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; -import { SocialsContainer, Socials } from "./styles"; +import { GithubLoginButton } from "react-social-login-buttons"; +import { Socials, SocialsContainer } from "./styles"; +import { GITHUB_CLIENT_ID } from "../../../settings"; +import { createRedirectUri } from "../../../utils/logic"; +import { OAuthProvider } from "../../../data/enums"; +import { generateRegisterState } from "../../../utils/session-storage"; + +export default function SocialButtons(props: { edition: string; uuid: string }) { + async function githubRegister() { + let authUrl = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}`; + authUrl += `&redirect_uri=${encodeURIComponent( + createRedirectUri(OAuthProvider.GITHUB, props) + )}`; + authUrl += "&scope=read:user%20user:email"; + authUrl += `&state=${generateRegisterState()}`; + + window.location.replace(authUrl); + } -export default function SocialButtons() { return ( - - + ); diff --git a/frontend/src/components/RegisterComponents/index.ts b/frontend/src/components/RegisterComponents/index.ts index f60693215..ae3f9c14d 100644 --- a/frontend/src/components/RegisterComponents/index.ts +++ b/frontend/src/components/RegisterComponents/index.ts @@ -5,3 +5,4 @@ export { default as ConfirmPassword } from "./InputFields/ConfirmPassword"; export { default as SocialButtons } from "./SocialButtons"; export { default as InfoText } from "./InfoText"; export { default as BadInviteLink } from "./BadInviteLink"; +export { default as AlreadyRegistered } from "./AlreadyRegistered"; diff --git a/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/FileField/FileField.tsx b/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/FileField/FileField.tsx new file mode 100644 index 000000000..e3c42f8c9 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/FileField/FileField.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { StudentFile } from "../../../../data/interfaces/questions"; +import { FileLink } from "./styles"; + +/** + * Component that renders a file with his link + */ +export default function FileField({ file }: { file: StudentFile }) { + return {file.filename}; +} diff --git a/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/FileField/index.ts b/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/FileField/index.ts new file mode 100644 index 000000000..0274a1d4a --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/FileField/index.ts @@ -0,0 +1 @@ +export { default } from "./FileField"; diff --git a/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/FileField/styles.ts b/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/FileField/styles.ts new file mode 100644 index 000000000..68e2b1423 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/FileField/styles.ts @@ -0,0 +1,11 @@ +import styled from "styled-components"; + +export const FileLink = styled.a` + width: fit-content; + color: var(--osoc_orange); + &:hover { + cursor: pointer; + color: var(--osoc_green); + transition: 200ms ease-out; + } +`; diff --git a/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/QuestionAnswer/Answer.tsx b/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/QuestionAnswer/Answer.tsx new file mode 100644 index 000000000..4ed53a317 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/QuestionAnswer/Answer.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { Question } from "../../../../data/interfaces/questions"; +import { Card } from "react-bootstrap"; +import FileField from "../FileField/FileField"; +import { QuestionAnswersContainer } from "./styles"; + +/** + * Component that removes the current student. + */ +export default function SomeTest({ question }: { question: Question }) { + return ( +
    + {question.answers.length === 0 && question.files.length === 0 ? null : ( + + +

    {question.question}

    +
    + + + {question.answers.map((answer, i) => ( +

    {answer}

    + ))} + {question.files.length === 0 ? null : ( +
    + {question.files.map((file, i) => ( + + ))} +
    + )} +
    +
    +
    + )} +
    + ); +} diff --git a/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/QuestionAnswer/index.ts b/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/QuestionAnswer/index.ts new file mode 100644 index 000000000..5260333fe --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/QuestionAnswer/index.ts @@ -0,0 +1 @@ +export { default } from "./Answer"; diff --git a/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/QuestionAnswer/styles.ts b/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/QuestionAnswer/styles.ts new file mode 100644 index 000000000..ced9b1a5e --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/QuestionAnswer/styles.ts @@ -0,0 +1,7 @@ +import styled from "styled-components"; + +export const QuestionAnswersContainer = styled.div` + display: flex; + width: 100%; + flex-direction: column; +`; diff --git a/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/QuestionsAndAnswers.tsx b/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/QuestionsAndAnswers.tsx new file mode 100644 index 000000000..c67bdd941 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/QuestionsAndAnswers.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { Card } from "react-bootstrap"; +import { Question } from "../../../data/interfaces/questions"; +import Answer from "./QuestionAnswer"; + +/** + * Component that removes the current student. + */ +export default function QuestionsAndAnswers({ questions }: { questions: Question[] }) { + return ( + + Questions + + {questions.map((question, i) => ( + + ))} + + + ); +} diff --git a/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/index.ts b/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/index.ts new file mode 100644 index 000000000..c4ca0790a --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/QuestionsAndAnswers/index.ts @@ -0,0 +1 @@ +export { default } from "./QuestionsAndAnswers"; diff --git a/frontend/src/components/StudentInfoComponents/RemoveStudentButton/RemoveStudentButton.tsx b/frontend/src/components/StudentInfoComponents/RemoveStudentButton/RemoveStudentButton.tsx new file mode 100644 index 000000000..0a75c2d9e --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/RemoveStudentButton/RemoveStudentButton.tsx @@ -0,0 +1,58 @@ +import React, { useState } from "react"; +import { removeStudent } from "../../../utils/api/students"; +import { useNavigate } from "react-router-dom"; +import { DeleteButton } from "../../Common/Buttons"; +import { Button, Modal } from "react-bootstrap"; +import { Student } from "../../../data/interfaces/students"; + +/** + * Component that removes the current student. + */ +export default function RemoveStudentButton(props: { + editionId: string; + studentId: number; + student: Student; +}) { + const navigate = useNavigate(); + const [show, setShow] = useState(false); + + /** + * Close the modal. + */ + const handleClose = () => setShow(false); + + /** + * Show the modal. + */ + const handleShow = () => setShow(true); + + /** + * Remove the current selected student and navigate back to the students page. + */ + async function handleRemoveStudent() { + await removeStudent(props.editionId, props.studentId); + navigate(`/editions/${props.editionId}/students/`); + } + + return ( +
    + + + + Are you sure you want to delete {props.student.firstName}{" "} + {props.student.lastName}? + + + + + + Remove {props.student.firstName} + + + + Remove Student +
    + ); +} diff --git a/frontend/src/components/StudentInfoComponents/StudentCopyLink/StudentCopyLink.tsx b/frontend/src/components/StudentInfoComponents/StudentCopyLink/StudentCopyLink.tsx new file mode 100644 index 000000000..5bf783740 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/StudentCopyLink/StudentCopyLink.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { StudentLink, CopyIcon, CopyLinkContainer } from "../StudentInformation/styles"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { faLink } from "@fortawesome/free-solid-svg-icons"; +import { toast } from "react-toastify"; + +/** + * Copy URL of current selected student. + */ +export default function StudentCopyLink() { + function copyStudentLink() { + navigator.clipboard.writeText(window.location.href); + toast.info("Student URL copied to clipboard!"); + } + return ( + + copy link + + + ); +} diff --git a/frontend/src/components/StudentInfoComponents/StudentInfo.tsx b/frontend/src/components/StudentInfoComponents/StudentInfo.tsx new file mode 100644 index 000000000..d2cddd099 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/StudentInfo.tsx @@ -0,0 +1,19 @@ +import { StudentListFilters } from "../StudentsComponents"; +import StudentInformation from "./StudentInformation/StudentInformation"; +import { StudentInfoPageContent } from "./styles"; +import { DragDropContext } from "react-beautiful-dnd"; + +/** + * Component that renders the students list and the information about the currently selected student. + * @param props all student, current student and all filters to handle the student information page. + */ +export default function StudentInfo(props: { studentId: number; editionId: string }) { + return ( + + {}}> + + + + + ); +} diff --git a/frontend/src/components/StudentInfoComponents/StudentInformation/StudentInformation.css b/frontend/src/components/StudentInfoComponents/StudentInformation/StudentInformation.css new file mode 100644 index 000000000..717d77b48 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/StudentInformation/StudentInformation.css @@ -0,0 +1,17 @@ +.CardContainer { + width: 100%; + background-color: var(--card-color) !important; + box-shadow: 5px 5px 15px #131329 !important; + border-radius: 5px !important; + border: 2px solid #1a1a36 !important; + margin-bottom: 2%; +} + +.CardHeader { + background-color: var(--card-color); + font-size: 30px; +} + +.CardBody { + background-color: var(--card-color); +} diff --git a/frontend/src/components/StudentInfoComponents/StudentInformation/StudentInformation.tsx b/frontend/src/components/StudentInfoComponents/StudentInformation/StudentInformation.tsx new file mode 100644 index 000000000..621ed6e57 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/StudentInformation/StudentInformation.tsx @@ -0,0 +1,292 @@ +import React, { useEffect, useState } from "react"; +import { + FullName, + FirstName, + LastName, + PreferedName, + SuggestionField, + StudentInformationContainer, + PersonalInfoFieldValue, + PersonalInfoFieldSubject, + RoleValue, + NameContainer, + SubjectFields, + SubjectValues, + PersonalInformation, + InfoHeadContainer, + AllName, + ActionContainer, + ActionsCard, + SuggestionTextColor, + SuggestionCoachAndArg, +} from "./styles"; +import { AdminDecisionContainer, CoachSuggestionContainer } from "../SuggestionComponents"; +import { Suggestion } from "../../../data/interfaces/suggestions"; +import { getSuggestionById, getSuggestions } from "../../../utils/api/suggestions"; +import RemoveStudentButton from "../RemoveStudentButton/RemoveStudentButton"; +import { useAuth, useSockets } from "../../../contexts"; +import { Role } from "../../../data/enums"; +import { Student } from "../../../data/interfaces/students"; +import { getStudent } from "../../../utils/api/students"; +import LoadSpinner from "../../Common/LoadSpinner"; +import { toast } from "react-toastify"; +import StudentCopyLink from "../StudentCopyLink/StudentCopyLink"; +import "./StudentInformation.css"; +import { Card } from "react-bootstrap"; +import StudentStateHistoryButton from "../StudentStateHistoryButton"; +import QuestionsAndAnswers from "../QuestionsAndAnswers"; +import { getQuestions } from "../../../utils/api/questions"; +import { Question } from "../../../data/interfaces/questions"; +import { EventType, RequestMethod, WebSocketEvent } from "../../../data/interfaces/websockets"; +import { useNavigate } from "react-router-dom"; + +const wsEventTypes = [EventType.STUDENT, EventType.STUDENT_SUGGESTION]; + +/** + * Component that renders all information of a student and all buttons to perform actions on this student. + */ +export default function StudentInformation(props: { studentId: number; editionId: string }) { + const { role } = useAuth(); + const { socket } = useSockets(); + const navigate = useNavigate(); + + const [questions, setQuestions] = useState([]); + const [suggestions, setSuggestions] = useState([]); + const [student, setStudent] = useState(undefined); + + async function getData() { + const studentResponse = await toast.promise(getStudent(props.editionId, props.studentId), { + error: "Failed to get details", + }); + const suggestionsResponse = await toast.promise( + getSuggestions(props.editionId, props.studentId), + { error: "Failed to get suggestions" } + ); + const answersResponse = await toast.promise( + getQuestions(props.editionId, props.studentId), + { error: "Failed to get suggestions" } + ); + setStudent(studentResponse); + setSuggestions(suggestionsResponse.suggestions); + setQuestions(answersResponse.qAndA); + } + + /** + * Get all info about this student without showing toasts + * Used in websockets + */ + async function getDataNoToasts() { + const student = await getStudent(props.editionId, props.studentId); + const suggestionsResponse = await getSuggestions(props.editionId, props.studentId); + setStudent(student); + setSuggestions(suggestionsResponse.suggestions); + } + + /** + * Get the string representation for a suggestion value. + * @param suggestion + */ + function suggestionToText(suggestion: number) { + if (suggestion === 0) { + return "Undecided"; + } else if (suggestion === 1) { + return "Yes"; + } else if (suggestion === 2) { + return "Maybe"; + } else if (suggestion === 3) { + return "No"; + } + } + + /** + * fetch suggestions whenever an edition id or student id changes. + */ + useEffect(() => { + getData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.studentId, props.editionId]); + + /** + * Delete a suggestion from the list + */ + function deleteSuggestion(id: string, suggestions: Suggestion[]): Suggestion[] { + return suggestions.filter(s => s.suggestionId.toString() !== id); + } + + /** + * Find a suggestion in the list & update it + */ + function findAndUpdateSuggestion(suggestion: Suggestion, list: Suggestion[]): Suggestion[] { + const index = list.findIndex(s => s.suggestionId === suggestion.suggestionId); + if (index === -1) return list; + + const copy = [...list]; + copy[index] = suggestion; + return copy; + } + + /** + * Websockets + */ + useEffect(() => { + function listener(event: MessageEvent) { + const data = JSON.parse(event.data) as WebSocketEvent; + + // Wrong type of event + if (!wsEventTypes.includes(data.eventType)) return; + // Event for another student + if (data.pathIds.studentId !== props.studentId.toString()) return; + + if (data.eventType === EventType.STUDENT) { + if (data.method === RequestMethod.DELETE) { + // Student deleted + navigate(`/editions/${props.editionId}/students`); + toast.info("This student was deleted by an admin."); + return; + } else if (data.method === RequestMethod.POST) { + // Suggestion or decision created + getDataNoToasts().then(); + } + } + + if (data.eventType === EventType.STUDENT_SUGGESTION) { + if (data.method === RequestMethod.DELETE) { + // Suggestion deleted + setSuggestions(deleteSuggestion(data.pathIds.suggestionId!, suggestions)); + } else if (data.method === RequestMethod.PATCH) { + // Suggestion edited + // Fetch the updated suggestion & update it in the list + getSuggestionById( + props.editionId, + props.studentId.toString(), + data.pathIds.suggestionId! + ).then(suggestion => + setSuggestions(findAndUpdateSuggestion(suggestion, suggestions)) + ); + } + } + } + + socket?.addEventListener("message", listener); + + function removeListener() { + if (socket) { + socket.removeEventListener("message", listener); + } + } + + return removeListener; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [socket, props.editionId, props.studentId, suggestions]); + + if (student === undefined) { + return ; + } else { + return ( + + + + + + {student.firstName} + {student.lastName} + + {student.preferredName !== null && ( +
    + {student.preferredName} +
    + )} + +
    +
    + + + Actions + + + {role === Role.ADMIN ? : null} + {role === Role.ADMIN ? ( + + ) : null} + + + +
    + + Suggestions + + {suggestions.map(suggestion => ( + + + {" "} + {suggestionToText(suggestion.suggestion)} + + + {suggestion.coach.name + + (suggestion.argumentation + ? ': "' + suggestion.argumentation + '"' + : "")} + + + ))} + {suggestions.length === 0 ? ( + + + No Suggestions yet + + + ) : null} + + + + Personal information + + + + Email + Phone number + Is an alumni? + + Wants to be student coach? + + + + + {student.emailAddress} + + + {student.phoneNumber} + + + {student.alumni ? "Yes" : "No"} + + + {student.wantsToBeStudentCoach ? "Yes" : "No"} + + + + + + + Skills + + {student.skills.map(skill => ( + {skill.name} + ))} + + + + {role === Role.ADMIN ? ( + + ) : null} +
    + ); + } +} diff --git a/frontend/src/components/StudentInfoComponents/StudentInformation/styles.ts b/frontend/src/components/StudentInfoComponents/StudentInformation/styles.ts new file mode 100644 index 000000000..3b84a9808 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/StudentInformation/styles.ts @@ -0,0 +1,136 @@ +import styled from "styled-components"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Card } from "react-bootstrap"; +import { SuggestionColor, SuggestionEnum } from "../../../data/enums/suggestions"; + +export const InfoHeadContainer = styled.div` + display: flex; + width: 100%; + margin-bottom: 1.5%; +`; + +export const StudentInformationContainer = styled.div` + width: 100%; + padding: 20px; +`; + +export const ActionsCard = styled(Card)` + max-width: 100%; +`; + +export const NameContainer = styled.div` + display: flex; + align-items: center; + margin-top: 1%; + margin-left: 1%; + width: 100%; +`; + +export const ActionContainer = styled.div` + align-items: flex-end; + flex-direction: column; + display: flex; + margin-top: 1%; + min-width: fit-content; +`; + +export const AllName = styled.div` + display: flex; + flex-direction: column; + margin-left: 2%; +`; + +export const FullName = styled.div` + display: flex; +`; + +export const FirstName = styled.span` + font-size: 250%; + padding-right: 10px; + color: white; +`; + +export const LastName = styled.span` + font-size: 250%; + padding-right: 5px; + color: white; +`; + +export const CopyLinkContainer = styled.div` + display: flex; + width: fit-content; + height: 40%; + align-items: center; + font-size: 12px; + &:hover { + cursor: pointer; + color: var(--osoc_green); + transition: 200ms ease-out; + } +`; + +export const StudentLink = styled.p` + font-size: 12px; +`; + +export const CopyIcon = styled(FontAwesomeIcon)` + margin-left: 0.35vh; + margin-bottom: 20%; +`; + +export const PreferedName = styled.p` + font-size: 20px; +`; + +export const SuggestionField = styled.div` + display: flex; + font-size: 20px; + margin-bottom: 1%; +`; + +export const SubjectFields = styled.div` + width: 22vh; + display: flex; + flex-direction: column; +`; + +export const PersonalInformation = styled.div` + display: flex; + flex-direction: row; +`; + +export const SubjectValues = styled.div` + width: 30vh; + display: flex; + flex-direction: column; +`; + +export const PersonalInfoFieldSubject = styled.p` + min-width: 30%; +`; + +export const PersonalInfoFieldValue = styled.p``; + +export const RoleValue = styled.p` + font-size: 100%; + margin-bottom: 1%; +`; + +export const DefinitiveDecisionContainer = styled.div` + width: 40%; +`; + +export const SuggestionTextColor = styled.p<{ suggestion: number }>` + color: ${p => + p.suggestion === SuggestionEnum.YES + ? SuggestionColor.YES + : p.suggestion === SuggestionEnum.MAYBE + ? SuggestionColor.MAYBE + : p.suggestion === SuggestionEnum.NO + ? SuggestionColor.NO + : "#FFFFFF"}; +`; + +export const SuggestionCoachAndArg = styled.p` + margin-left: 10px; +`; diff --git a/frontend/src/components/StudentInfoComponents/StudentStateHistoryButton/StudentStateHistoryButton.tsx b/frontend/src/components/StudentInfoComponents/StudentStateHistoryButton/StudentStateHistoryButton.tsx new file mode 100644 index 000000000..db2e88355 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/StudentStateHistoryButton/StudentStateHistoryButton.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { OrangeButton } from "../../Common/Buttons"; +import { StateContainer, StateTitle } from "./styles"; +import { useNavigate } from "react-router-dom"; + +export default function StudentStateHistoryButton(props: { editionId: string; studentId: number }) { + const navigate = useNavigate(); + + return ( +
    + State history of student + + + navigate(`/editions/${props.editionId}/students/${props.studentId}/states`) + } + > + State history + + +
    + ); +} diff --git a/frontend/src/components/StudentInfoComponents/StudentStateHistoryButton/index.ts b/frontend/src/components/StudentInfoComponents/StudentStateHistoryButton/index.ts new file mode 100644 index 000000000..85b50900d --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/StudentStateHistoryButton/index.ts @@ -0,0 +1 @@ +export { default } from "./StudentStateHistoryButton"; diff --git a/frontend/src/components/StudentInfoComponents/StudentStateHistoryButton/styles.ts b/frontend/src/components/StudentInfoComponents/StudentStateHistoryButton/styles.ts new file mode 100644 index 000000000..ded0a78c2 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/StudentStateHistoryButton/styles.ts @@ -0,0 +1,10 @@ +import styled from "styled-components"; + +export const StateContainer = styled.div` + width: 40%; +`; + +export const StateTitle = styled.p` + margin-top: 2%; + font-size: 20px; +`; diff --git a/frontend/src/components/StudentInfoComponents/SuggestionComponents/AdminDecisionContainer/AdminDecisionContainer.tsx b/frontend/src/components/StudentInfoComponents/SuggestionComponents/AdminDecisionContainer/AdminDecisionContainer.tsx new file mode 100644 index 000000000..9f3757b14 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/SuggestionComponents/AdminDecisionContainer/AdminDecisionContainer.tsx @@ -0,0 +1,122 @@ +import React, { useState } from "react"; +import { Button, Modal } from "react-bootstrap"; +import { DefinitiveDecisionContainer } from "../../StudentInformation/styles"; +import { SuggestionButtons, ConfirmActionTitle } from "./styles"; +import { YesButton, MaybeButton, NoButton } from "../CoachSuggestionContainer/styles"; +import { confirmStudent } from "../../../../utils/api/suggestions"; +import { useParams } from "react-router-dom"; +import { CreateButton } from "../../../Common/Buttons"; +import { toast } from "react-toastify"; + +/** + * Make definitive decision on the current student based on the selected decision value. + * Only admins can see this component. + */ +export default function AdminDecisionContainer() { + const params = useParams(); + const [show, setShow] = useState(false); + const [clickedButtonText, setClickedButtonText] = useState(""); + + /** + * Close the modal. + */ + function handleClose() { + setShow(false); + setClickedButtonText(""); + } + + /** + * Show the modal. + */ + function handleShow(event: React.MouseEvent) { + event.preventDefault(); + setShow(true); + } + + /** + * Make definitive decision on the current student based on the selected decision value. + */ + function handleClick(event: React.MouseEvent) { + event.preventDefault(); + const button: HTMLButtonElement = event.currentTarget; + setClickedButtonText(button.innerText); + } + + async function makeDecision() { + let decisionNum: number; + if (clickedButtonText === "Undecided") { + decisionNum = 0; + } else if (clickedButtonText === "Yes") { + decisionNum = 1; + } else if (clickedButtonText === "Maybe") { + decisionNum = 2; + } else { + decisionNum = 3; + } + await toast.promise(confirmStudent(params.editionId!, params.id!, decisionNum), { + error: "Failed to send decision", + pending: "Sending decision", + success: "Decision successfully sent", + }); + setClickedButtonText(""); + setShow(false); + } + + return ( +
    + + + Definitive decision on student + + + Click on one of the buttons to mark your decision + + ) => handleClick(e)} + > + Yes + + ) => handleClick(e)} + > + Maybe + + ) => handleClick(e)} + > + No + + + + + +
    + {clickedButtonText ? ( + + Confirm {clickedButtonText}? + + ) : ( + + Confirm + + )} +
    +
    +
    + Definitive decision by admin + + + Confirm + + +
    + ); +} diff --git a/frontend/src/components/StudentInfoComponents/SuggestionComponents/AdminDecisionContainer/index.ts b/frontend/src/components/StudentInfoComponents/SuggestionComponents/AdminDecisionContainer/index.ts new file mode 100644 index 000000000..cc355e060 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/SuggestionComponents/AdminDecisionContainer/index.ts @@ -0,0 +1 @@ +export { default } from "./AdminDecisionContainer"; diff --git a/frontend/src/components/StudentInfoComponents/SuggestionComponents/AdminDecisionContainer/styles.ts b/frontend/src/components/StudentInfoComponents/SuggestionComponents/AdminDecisionContainer/styles.ts new file mode 100644 index 000000000..2b0559ea3 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/SuggestionComponents/AdminDecisionContainer/styles.ts @@ -0,0 +1,21 @@ +import styled from "styled-components"; +import { Button } from "react-bootstrap"; + +export const SuggestionButtons = styled.div` + display: flex; + flex-direction: row; + width: 100%; + margin-top: 2%; +`; + +export const ConfirmButton = styled(Button)` + width: 30%; + height: 30%; + margin-left: 2%; + margin-right: 2%; +`; + +export const ConfirmActionTitle = styled.p` + margin-top: 2%; + font-size: 20px; +`; diff --git a/frontend/src/components/StudentInfoComponents/SuggestionComponents/CoachSuggestionContainer/CoachSuggestionContainer.tsx b/frontend/src/components/StudentInfoComponents/SuggestionComponents/CoachSuggestionContainer/CoachSuggestionContainer.tsx new file mode 100644 index 000000000..cfbdb2e80 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/SuggestionComponents/CoachSuggestionContainer/CoachSuggestionContainer.tsx @@ -0,0 +1,116 @@ +import { Button, ButtonGroup, Modal } from "react-bootstrap"; +import React, { useState } from "react"; +import { Student } from "../../../../data/interfaces/students"; +import { makeSuggestion } from "../../../../utils/api/students"; +import { useParams } from "react-router-dom"; +import { SuggestionActionTitle, YesButton, MaybeButton, NoButton } from "./styles"; +import { CreateButton } from "../../../Common/Buttons"; +import { FormControl } from "../../../Common/Forms"; +import { toast } from "react-toastify"; + +interface Props { + student: Student; +} + +/** + * Component for functionality of suggestions. + * @param props current student + */ +export default function CoachSuggestionContainer(props: Props) { + const params = useParams(); + const [show, setShow] = useState(false); + const [argumentation, setArgumentation] = useState(""); + const [clickedButtonText, setClickedButtonText] = useState(""); + + /** + * Close the modal. + */ + const handleClose = () => setShow(false); + + /** + * Show the modal. + */ + function handleShow(event: React.MouseEvent) { + event.preventDefault(); + const button: HTMLButtonElement = event.currentTarget; + setClickedButtonText(button.innerText); + setShow(true); + } + + /** + * Make suggestion on the current student based on the selected suggestion value. + */ + async function doSuggestion() { + let suggestionNum: number; + if (clickedButtonText === "Yes") { + suggestionNum = 1; + } else if (clickedButtonText === "Maybe") { + suggestionNum = 2; + } else { + suggestionNum = 3; + } + await toast.promise( + makeSuggestion(params.editionId!, params.id!, suggestionNum, argumentation), + { + error: "Failed to send suggestion", + pending: "Sending suggestion", + success: "Suggestion successfully sent", + } + ); + setArgumentation(""); + setShow(false); + } + + return ( +
    + + + + Suggestion "{clickedButtonText}" for{" "} + {props.student.firstName + " " + props.student.lastName} + + + + Why are you giving this decision for this student? + { + setArgumentation(e.target.value); + }} + placeholder="Place your argumentation here..." + /> + * This field isn't required + + + + Save Suggestion + + + Make a suggestion on this student + + + ) => handleShow(e)} + > + Yes + + ) => handleShow(e)} + > + Maybe + + ) => handleShow(e)} + > + No + + +
    + ); +} diff --git a/frontend/src/components/StudentInfoComponents/SuggestionComponents/CoachSuggestionContainer/index.ts b/frontend/src/components/StudentInfoComponents/SuggestionComponents/CoachSuggestionContainer/index.ts new file mode 100644 index 000000000..fd6e1cdc7 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/SuggestionComponents/CoachSuggestionContainer/index.ts @@ -0,0 +1 @@ +export { default } from "./CoachSuggestionContainer"; diff --git a/frontend/src/components/StudentInfoComponents/SuggestionComponents/CoachSuggestionContainer/styles.ts b/frontend/src/components/StudentInfoComponents/SuggestionComponents/CoachSuggestionContainer/styles.ts new file mode 100644 index 000000000..52b992df0 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/SuggestionComponents/CoachSuggestionContainer/styles.ts @@ -0,0 +1,32 @@ +import styled from "styled-components"; +import { Button } from "react-bootstrap"; + +export const SuggestionActionTitle = styled.p` + font-size: 20px; +`; + +export const YesButton = styled(Button)` + background-color: var(--osoc_green); + color: black; + height: 100%; + width: 100%; + margin-right: 2%; +`; + +export const MaybeButton = styled(Button)` + background-color: var(--osoc_orange); + color: black; + height: 100%; + width: 100%; + margin-left: 2%; + margin-right: 2%; +`; + +export const NoButton = styled(Button)` + background-color: var(--osoc_red); + color: black; + height: 100%; + width: 100%; + margin-left: 2%; + margin-right: 2%; +`; diff --git a/frontend/src/components/StudentInfoComponents/SuggestionComponents/index.ts b/frontend/src/components/StudentInfoComponents/SuggestionComponents/index.ts new file mode 100644 index 000000000..0c2fa27fb --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/SuggestionComponents/index.ts @@ -0,0 +1,2 @@ +export { default as AdminDecisionContainer } from "./AdminDecisionContainer"; +export { default as CoachSuggestionContainer } from "./CoachSuggestionContainer"; diff --git a/frontend/src/components/StudentInfoComponents/styles.ts b/frontend/src/components/StudentInfoComponents/styles.ts new file mode 100644 index 000000000..ee8d68c10 --- /dev/null +++ b/frontend/src/components/StudentInfoComponents/styles.ts @@ -0,0 +1,15 @@ +import styled from "styled-components"; + +export const StudentRemoveButton = styled.button` + width: 150px; + height: 35px; + right: 5%; + top: 12%; + color: white; + background-color: var(--osoc_red); + border: 4px solid var(--osoc_red); +`; + +export const StudentInfoPageContent = styled.div` + display: flex; +`; diff --git a/frontend/src/components/StudentsComponents/StudentList/StudentCard/StudentCard.tsx b/frontend/src/components/StudentsComponents/StudentList/StudentCard/StudentCard.tsx new file mode 100644 index 000000000..c788d42a4 --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentList/StudentCard/StudentCard.tsx @@ -0,0 +1,60 @@ +import React, { useEffect, useState } from "react"; +import { + CardStudent, + CardStudentInfo, + CardVerticalContainer, + CardHorizontalContainer, + CardStudentName, +} from "./styles"; +import { useNavigate, useParams } from "react-router-dom"; +import { Student } from "../../../../data/interfaces/students"; +import SuggestionProgressBar from "../SuggestionProgressBar"; + +interface Props { + student: Student; +} + +/** + * Card component that will be used to show a student in the students list. + * @param props all information about a student. + */ +export default function StudentCard(props: Props) { + const params = useParams(); + const navigate = useNavigate(); + const [nameColor, setNameColor] = useState(""); + + useEffect(() => { + const final = props.student.finalDecision; + if (final === 0) { + setNameColor("white"); + } else if (final === 1) { + setNameColor("#44dba4"); // osoc green + } else if (final === 2) { + setNameColor("#fcb70f"); // osoc orange + } else if (final === 3) { + setNameColor("#f14a3b"); // osoc red + } + }, [props.student.finalDecision]); + + return ( + <> + + navigate(`/editions/${params.editionId}/students/${props.student.studentId}`) + } + > + {/* */} + + + + + {props.student.firstName} {props.student.lastName} + + + + + + + + ); +} diff --git a/frontend/src/components/StudentsComponents/StudentList/StudentCard/index.ts b/frontend/src/components/StudentsComponents/StudentList/StudentCard/index.ts new file mode 100644 index 000000000..1ec94794f --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentList/StudentCard/index.ts @@ -0,0 +1 @@ +export { default } from "./StudentCard"; diff --git a/frontend/src/components/StudentsComponents/StudentList/StudentCard/styles.ts b/frontend/src/components/StudentsComponents/StudentList/StudentCard/styles.ts new file mode 100644 index 000000000..9a44590df --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentList/StudentCard/styles.ts @@ -0,0 +1,40 @@ +import styled from "styled-components"; + +export const CardStudent = styled.div` + display: flex; + flex-direction: row; + margin: 10px; + &:hover { + cursor: pointer; + } + background-color: var(--card-color); + box-shadow: 5px 5px 15px #131329; + border-radius: 5px; + border: 2px solid #1a1a36; +`; + +export const CardStudentInfo = styled.div` + display: flex; + width: 100%; + min-height: 75px; + flex-direction: row; +`; + +export const CardVerticalContainer = styled.div` + width: 100%; + display: flex; + flex-direction: column; +`; + +export const CardHorizontalContainer = styled.div` + display: flex; + width: 100%; + flex-direction: row; +`; + +export const CardStudentName = styled.p` + width: 80%; + font-size: 20px; + margin-left: 5%; + margin-top: 1%; +`; diff --git a/frontend/src/components/StudentsComponents/StudentList/StudentList.tsx b/frontend/src/components/StudentsComponents/StudentList/StudentList.tsx new file mode 100644 index 000000000..ece591d17 --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentList/StudentList.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import { StudentCard } from "../index"; +import { StudentCardsList } from "./styles"; +import { Student } from "../../../data/interfaces/students"; +import InfiniteScroll from "react-infinite-scroller"; +import { Draggable, Droppable } from "react-beautiful-dnd"; +import LoadSpinner from "../../Common/LoadSpinner"; +import { isReadonlyEdition } from "../../../utils/logic"; +import { useParams } from "react-router-dom"; +import { useAuth } from "../../../contexts"; + +interface Props { + students: Student[]; + moreDataAvailable: boolean; + getMoreData: (page: number) => void; +} + +/** + * Component that renders the list of students in the sidebar. + * @param props the students that need to be rendered. + */ +export default function StudentList(props: Props) { + const params = useParams(); + const editionId = params.editionId; + const projectId = params.projectId; + + const { editions } = useAuth(); + + const notDraggable = isReadonlyEdition(editionId, editions) || projectId === undefined; + + return ( + + } + useWindow={false} + initialLoad={true} + > + + {(provided, snapshot) => ( +
    + {props.students.map((student, index) => ( + + {(provided, snapshot) => ( +
    + +
    + )} +
    + ))} + {provided.placeholder} +
    + )} +
    +
    +
    + ); +} diff --git a/frontend/src/components/StudentsComponents/StudentList/SuggestionProgressBar/SuggestionProgressBar.css b/frontend/src/components/StudentsComponents/StudentList/SuggestionProgressBar/SuggestionProgressBar.css new file mode 100644 index 000000000..67cda2714 --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentList/SuggestionProgressBar/SuggestionProgressBar.css @@ -0,0 +1,11 @@ +#green-progress { + background-color: var(--osoc_green); +} + +#orange-progress { + background-color: var(--osoc_orange); +} + +#red-progress { + background-color: var(--osoc_red); +} diff --git a/frontend/src/components/StudentsComponents/StudentList/SuggestionProgressBar/SuggestionProgressBar.tsx b/frontend/src/components/StudentsComponents/StudentList/SuggestionProgressBar/SuggestionProgressBar.tsx new file mode 100644 index 000000000..e3dfd51bf --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentList/SuggestionProgressBar/SuggestionProgressBar.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { NrSuggestions } from "../../../../data/interfaces/students"; +import { SuggestionBarContainer } from "./styles"; +import { ProgressBar } from "react-bootstrap"; +import "./SuggestionProgressBar.css"; + +interface Props { + nrOfSuggestions: NrSuggestions; +} + +/** + * Count the total amount of suggestion (yes, maybe and no). + * @param suggestions + */ +function totalSuggestions(suggestions: NrSuggestions) { + let total: number = 0; + Object.entries(suggestions).forEach(([key, value]) => { + total += value; + }); + return total; +} + +/** + * Component that shows a progressBar that weights all suggestions and shows how many of each there are. + * @param props all the suggestions. + */ +export default function SuggestionProgressBar(props: Props) { + const amountSuggestions = totalSuggestions(props.nrOfSuggestions); + const frequencyYes = (props.nrOfSuggestions.yes * 100) / amountSuggestions; + const frequencyMaybe = (props.nrOfSuggestions.maybe * 100) / amountSuggestions; + const frequencyNo = (props.nrOfSuggestions.no * 100) / amountSuggestions; + + return ( + + + + + + + + ); +} diff --git a/frontend/src/components/StudentsComponents/StudentList/SuggestionProgressBar/index.ts b/frontend/src/components/StudentsComponents/StudentList/SuggestionProgressBar/index.ts new file mode 100644 index 000000000..112160452 --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentList/SuggestionProgressBar/index.ts @@ -0,0 +1 @@ +export { default } from "./SuggestionProgressBar"; diff --git a/frontend/src/components/StudentsComponents/StudentList/SuggestionProgressBar/styles.ts b/frontend/src/components/StudentsComponents/StudentList/SuggestionProgressBar/styles.ts new file mode 100644 index 000000000..202a0f2a9 --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentList/SuggestionProgressBar/styles.ts @@ -0,0 +1,7 @@ +import styled from "styled-components"; + +export const SuggestionBarContainer = styled.div` + width: 90%; + background: transparent; + margin-left: 5%; +`; diff --git a/frontend/src/components/StudentsComponents/StudentList/index.ts b/frontend/src/components/StudentsComponents/StudentList/index.ts new file mode 100644 index 000000000..307a2dade --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentList/index.ts @@ -0,0 +1,2 @@ +export { default as StudentCard } from "./StudentCard"; +export { default } from "./StudentList"; diff --git a/frontend/src/components/StudentsComponents/StudentList/styles.ts b/frontend/src/components/StudentsComponents/StudentList/styles.ts new file mode 100644 index 000000000..46a958137 --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentList/styles.ts @@ -0,0 +1,7 @@ +import styled from "styled-components"; + +export const StudentCardsList = styled.div` + height: 60%; + overflow-y: auto; + margin-top: 2%; +`; diff --git a/frontend/src/components/StudentsComponents/StudentListFilters/AlumniFilter/AlumniFilter.tsx b/frontend/src/components/StudentsComponents/StudentListFilters/AlumniFilter/AlumniFilter.tsx new file mode 100644 index 000000000..690fe96d4 --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentListFilters/AlumniFilter/AlumniFilter.tsx @@ -0,0 +1,37 @@ +import { Form } from "react-bootstrap"; +import React from "react"; +import { setAlumniFilterStorage } from "../../../../utils/session-storage/student-filters"; + +/** + * Component that filters the students list based on the alumni field. + * @param alumniFilter + * @param setAlumniFilter + * @param setPage Function to set the page to fetch next + */ +export default function AlumniFilter({ + alumniFilter, + setAlumniFilter, + setPage, +}: { + alumniFilter: boolean; + setAlumniFilter: (value: boolean) => void; + setPage: (page: number) => void; +}) { + return ( +
    + { + setAlumniFilter(e.target.checked); + setAlumniFilterStorage(String(e.target.checked)); + e.target.checked = alumniFilter; + setPage(0); + }} + /> +
    + ); +} diff --git a/frontend/src/components/StudentsComponents/StudentListFilters/AlumniFilter/index.ts b/frontend/src/components/StudentsComponents/StudentListFilters/AlumniFilter/index.ts new file mode 100644 index 000000000..93675ced5 --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentListFilters/AlumniFilter/index.ts @@ -0,0 +1 @@ +export { default } from "./AlumniFilter"; diff --git a/frontend/src/components/StudentsComponents/StudentListFilters/ConfirmFilters/ConfirmFilters.tsx b/frontend/src/components/StudentsComponents/StudentListFilters/ConfirmFilters/ConfirmFilters.tsx new file mode 100644 index 000000000..21a7589e0 --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentListFilters/ConfirmFilters/ConfirmFilters.tsx @@ -0,0 +1,54 @@ +import React, { useEffect, useState } from "react"; +import { FilterConfirmsDropdownContainer, FilterConfirms, ConfirmsTitle } from "../styles"; +import { DropdownRole } from "../RolesFilter/RolesFilter"; +import Select, { MultiValue } from "react-select"; +import { setConfirmFilterStorage } from "../../../../utils/session-storage/student-filters"; + +/** + * Component that filters the students list based on confirmation. + */ +export default function ConfirmFilters({ + confirmFilter, + setConfirmFilter, + setPage, +}: { + confirmFilter: DropdownRole[]; + setConfirmFilter: (value: DropdownRole[]) => void; + setPage: (page: number) => void; +}) { + const [confirms, setConfirms] = useState([]); + + useEffect(() => { + setConfirms([ + { label: "Yes", value: 1 }, + { label: "Maybe", value: 2 }, + { label: "No", value: 3 }, + { label: "Undecided", value: 0 }, + ]); + }, []); + + function handleRolesChange(event: MultiValue): void { + const allCheckedRoles: DropdownRole[] = []; + event.forEach(dropdownRole => allCheckedRoles.push(dropdownRole)); + setConfirmFilter(allCheckedRoles); + setPage(0); + setConfirmFilterStorage(JSON.stringify(allCheckedRoles)); + } + + return ( + + Confirmed + + { + handleRolesChange(e); + setPage(0); + }} + /> + + + ); +} diff --git a/frontend/src/components/StudentsComponents/StudentListFilters/RolesFilter/index.ts b/frontend/src/components/StudentsComponents/StudentListFilters/RolesFilter/index.ts new file mode 100644 index 000000000..992a7acbd --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentListFilters/RolesFilter/index.ts @@ -0,0 +1 @@ +export { default } from "./RolesFilter"; diff --git a/frontend/src/components/StudentsComponents/StudentListFilters/StudentCoachVolunteerFilter/StudentCoachVolunteerFilter.tsx b/frontend/src/components/StudentsComponents/StudentListFilters/StudentCoachVolunteerFilter/StudentCoachVolunteerFilter.tsx new file mode 100644 index 000000000..59514983a --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentListFilters/StudentCoachVolunteerFilter/StudentCoachVolunteerFilter.tsx @@ -0,0 +1,37 @@ +import { Form } from "react-bootstrap"; +import React from "react"; +import { setStudentCoachVolunteerFilterStorage } from "../../../../utils/session-storage/student-filters"; + +/** + * Component that filters the students list based on the student coach field. + * @param studentCoachVolunteerFilter + * @param setStudentCoachVolunteerFilter + * @param setPage Function to set the page to fetch next + */ +export default function StudentCoachVolunteerFilter({ + studentCoachVolunteerFilter, + setStudentCoachVolunteerFilter, + setPage, +}: { + studentCoachVolunteerFilter: boolean; + setStudentCoachVolunteerFilter: (value: boolean) => void; + setPage: (page: number) => void; +}) { + return ( +
    + { + setStudentCoachVolunteerFilter(e.target.checked); + setStudentCoachVolunteerFilterStorage(String(e.target.checked)); + e.target.checked = studentCoachVolunteerFilter; + setPage(0); + }} + /> +
    + ); +} diff --git a/frontend/src/components/StudentsComponents/StudentListFilters/StudentCoachVolunteerFilter/index.ts b/frontend/src/components/StudentsComponents/StudentListFilters/StudentCoachVolunteerFilter/index.ts new file mode 100644 index 000000000..ffc91985f --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentListFilters/StudentCoachVolunteerFilter/index.ts @@ -0,0 +1 @@ +export { default } from "./StudentCoachVolunteerFilter"; diff --git a/frontend/src/components/StudentsComponents/StudentListFilters/StudentListFilters.css b/frontend/src/components/StudentsComponents/StudentListFilters/StudentListFilters.css new file mode 100644 index 000000000..d4814782f --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentListFilters/StudentListFilters.css @@ -0,0 +1,4 @@ +.form-check { + margin-left: 5%; + margin-bottom: 1vh; +} diff --git a/frontend/src/components/StudentsComponents/StudentListFilters/StudentListFilters.tsx b/frontend/src/components/StudentsComponents/StudentListFilters/StudentListFilters.tsx new file mode 100644 index 000000000..d9cfec72a --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentListFilters/StudentListFilters.tsx @@ -0,0 +1,320 @@ +import React, { useEffect, useState } from "react"; +import StudentList from "../StudentList"; +import { Form } from "react-bootstrap"; +import { FilterControls, MessageDiv, StudentListLinebreak, StudentListSideMenu } from "./styles"; +import AlumniFilter from "./AlumniFilter/AlumniFilter"; +import StudentCoachVolunteerFilter from "./StudentCoachVolunteerFilter/StudentCoachVolunteerFilter"; +import NameFilter from "./NameFilter/NameFilter"; +import RolesFilter, { DropdownRole } from "./RolesFilter/RolesFilter"; +import "./StudentListFilters.css"; +import ResetFiltersButton from "./ResetFiltersButton/ResetFiltersButton"; +import { Student } from "../../../data/interfaces/students"; +import { useParams } from "react-router-dom"; +import { toast } from "react-toastify"; +import { getStudent, getStudents } from "../../../utils/api/students"; +import SuggestedForFilter from "./SuggestedForFilter/SuggestedForFilter"; +import { + getAlumniFilter, + getConfirmFilter, + getNameFilter, + getRolesFilter, + getStudentCoachVolunteerFilter, + getSuggestedFilter, +} from "../../../utils/session-storage/student-filters"; +import { useSockets } from "../../../contexts"; +import { EventType, RequestMethod, WebSocketEvent } from "../../../data/interfaces/websockets"; +import ConfirmFilters from "./ConfirmFilters/ConfirmFilters"; +import LoadSpinner from "../../Common/LoadSpinner"; + +// Types of events accepted by this websocket +const wsEventTypes = [EventType.STUDENT, EventType.STUDENT_SUGGESTION]; + +/** + * Component that shows the sidebar with all the filters and student list. + */ +export default function StudentListFilters() { + const params = useParams(); + const { socket } = useSockets(); + const [allStudents, setAllStudents] = useState([]); + const [students, setStudents] = useState([]); + const [loading, setLoading] = useState(false); + const [moreDataAvailable, setMoreDataAvailable] = useState(true); + const [allDataFetched, setAllDataFetched] = useState(false); + const [page, setPage] = useState(0); + const [controller, setController] = useState(undefined); + + const [nameFilter, setNameFilter] = useState(getNameFilter()); + const [rolesFilter, setRolesFilter] = useState(getRolesFilter()); + const [alumniFilter, setAlumniFilter] = useState(getAlumniFilter()); + const [studentCoachVolunteerFilter, setStudentCoachVolunteerFilter] = useState( + getStudentCoachVolunteerFilter() + ); + const [suggestedFilter, setSuggestedFilter] = useState(getSuggestedFilter()); + const [confirmFilter, setConfirmFilter] = useState(getConfirmFilter()); + + /** + * Request all students with selected filters + */ + async function getData(requested: number, edChange: boolean = false) { + const filterChanged = requested === -1; + const requestedPage = requested === -1 ? 0 : page; + + if (loading && !filterChanged) { + return; + } + + setLoading(true); + + if (allDataFetched && !edChange) { + const tempStudents = allStudents + .filter(student => + (student.firstName + " " + student.lastName) + .toUpperCase() + .includes(nameFilter!.toUpperCase()) + ) + .filter(student => !alumniFilter || student.alumni === alumniFilter) + .filter( + student => + !studentCoachVolunteerFilter || + student.wantsToBeStudentCoach === studentCoachVolunteerFilter + ); + + let tempStudents2: Student[]; + if (rolesFilter.length === 0) { + tempStudents2 = tempStudents; + } else { + tempStudents2 = []; + for (const student of tempStudents) { + let keep = false; + for (const skill of student.skills) { + for (const role of rolesFilter) { + if (role.value === skill.skillId) { + keep = true; + } + } + } + if (keep) { + tempStudents2.push(student); + } + } + } + if (confirmFilter.length === 0) { + setStudents(tempStudents2); + } else { + const finalStudents = []; + for (const student of tempStudents2) { + let keep = false; + for (const status of confirmFilter) { + if (student.finalDecision === status.value) { + keep = true; + } + } + if (keep) { + finalStudents.push(student); + } + } + setStudents(finalStudents); + } + + setMoreDataAvailable(false); + setLoading(false); + return; + } + + if (controller !== undefined) { + controller.abort(); + } + const newController = new AbortController(); + setController(newController); + + const response = await toast.promise( + getStudents( + params.editionId!, + nameFilter, + rolesFilter, + alumniFilter, + studentCoachVolunteerFilter, + suggestedFilter, + confirmFilter, + requestedPage, + newController + ), + { error: "Failed to retrieve students" } + ); + + if (response !== null) { + if (response.students.length === 0 && !filterChanged) { + setMoreDataAvailable(false); + } else { + setMoreDataAvailable(true); + } + if (requestedPage === 0 || filterChanged) { + setStudents(response.students); + } else { + setStudents(students.concat(response.students)); + } + + // If no filters are set, allStudents can be changed + if ( + nameFilter === "" && + rolesFilter.length === 0 && + confirmFilter.length === 0 && + !alumniFilter && + !studentCoachVolunteerFilter && + !suggestedFilter + ) { + if (response.students.length === 0) { + setAllDataFetched(true); + } + if (requestedPage === 0) { + setAllStudents(response.students); + } else { + setAllStudents(allStudents.concat(response.students)); + } + } + setPage(requestedPage + 1); + } else { + setMoreDataAvailable(false); + } + setLoading(false); + } + + /** + * fetch students again when a filter changes + */ + useEffect(() => { + setPage(0); + setMoreDataAvailable(true); + getData(-1, false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nameFilter, rolesFilter, alumniFilter, studentCoachVolunteerFilter, confirmFilter]); + + useEffect(() => { + refresh(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [params.editionId, suggestedFilter]); + + function refresh() { + setStudents([]); + setAllStudents([]); + setPage(0); + setAllDataFetched(false); + setMoreDataAvailable(true); + getData(-1, true); + } + + let list; + if (students.length === 0) { + if (loading) { + list = ; + } else { + list = No students found; + } + } else { + list = ( + + ); + } + + /** + * Find a student with a specific id and update its data + */ + function findAndUpdate(list: Student[], student: Student): Student[] { + const index = list.findIndex(s => s.studentId === student.studentId); + if (index === -1) return list; + + const copy = [...list]; + copy[index] = student; + return copy; + } + + /** + * Find a student with a specific id and delete it from the list + */ + function findAndDelete(id: string, list: Student[]): Student[] { + return list.filter(s => s.studentId.toString() !== id); + } + + useEffect(() => { + function listener(event: MessageEvent) { + const data = JSON.parse(event.data) as WebSocketEvent; + + if (!wsEventTypes.includes(data.eventType)) return; + + // Student was deleted + if (data.eventType === EventType.STUDENT) { + if (data.method === RequestMethod.DELETE) { + setAllStudents(findAndDelete(data.pathIds.studentId!, allStudents)); + setStudents(findAndDelete(data.pathIds.studentId!, students)); + return; + } + } + + // Everything else: the student was updated, or a suggestion was changed/deleted + // Handle both of these as re-fetching the student + getStudent(params.editionId!, parseInt(data.pathIds.studentId!)).then(student => { + setAllStudents(findAndUpdate(allStudents, student)); + setStudents(findAndUpdate(students, student)); + }); + } + + socket?.addEventListener("message", listener); + + function removeListener() { + if (socket) { + socket.removeEventListener("message", listener); + } + } + + return removeListener; + }, [socket, allStudents, students, params.editionId]); + + return ( + + + + + + + + + + + + + + {list} + + ); +} diff --git a/frontend/src/components/StudentsComponents/StudentListFilters/SuggestedForFilter/SuggestedForFilter.tsx b/frontend/src/components/StudentsComponents/StudentListFilters/SuggestedForFilter/SuggestedForFilter.tsx new file mode 100644 index 000000000..a16bd15db --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentListFilters/SuggestedForFilter/SuggestedForFilter.tsx @@ -0,0 +1,33 @@ +import { Form } from "react-bootstrap"; +import React from "react"; +import { setSuggestedFilterStorage } from "../../../../utils/session-storage/student-filters"; + +/** + * Component that filters the students list based on the suggested for field. + * @param suggestedFilter + * @param setSuggestedFilter + */ +export default function SuggestedForFilter({ + suggestedFilter, + setSuggestedFilter, +}: { + suggestedFilter: boolean; + setSuggestedFilter: (value: boolean) => void; +}) { + return ( +
    + { + setSuggestedFilter(e.target.checked); + setSuggestedFilterStorage(String(e.target.checked)); + e.target.checked = suggestedFilter; + }} + /> +
    + ); +} diff --git a/frontend/src/components/StudentsComponents/StudentListFilters/SuggestedForFilter/index.ts b/frontend/src/components/StudentsComponents/StudentListFilters/SuggestedForFilter/index.ts new file mode 100644 index 000000000..2c58462fc --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentListFilters/SuggestedForFilter/index.ts @@ -0,0 +1 @@ +export { default } from "./SuggestedForFilter"; diff --git a/frontend/src/components/StudentsComponents/StudentListFilters/index.ts b/frontend/src/components/StudentsComponents/StudentListFilters/index.ts new file mode 100644 index 000000000..59d44301a --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentListFilters/index.ts @@ -0,0 +1,6 @@ +export { default } from "./StudentListFilters"; +export { default as AlumniFilter } from "./AlumniFilter"; +export { default as StudentCoachVolunteerFilter } from "./StudentCoachVolunteerFilter"; +export { default as NameFilter } from "./NameFilter"; +export { default as ResetFiltersButton } from "./ResetFiltersButton"; +export { default as RolesFilter } from "./RolesFilter"; diff --git a/frontend/src/components/StudentsComponents/StudentListFilters/styles.ts b/frontend/src/components/StudentsComponents/StudentListFilters/styles.ts new file mode 100644 index 000000000..f33ba394e --- /dev/null +++ b/frontend/src/components/StudentsComponents/StudentListFilters/styles.ts @@ -0,0 +1,89 @@ +import styled from "styled-components"; + +export const StudentListSideMenu = styled.div` + display: flex; + position: sticky; + flex-direction: column; + width: 35%; + min-width: 35vh; + max-width: 50vh; + height: 100vh; +`; + +export const StudentListLinebreak = styled.div` + height: 1px; + background-color: white; + width: 90%; + align-self: center; +`; + +export const FilterStudentName = styled.div` + width: 90%; + display: flex; + margin-top: 15px; + align-self: center; + align-items: center; +`; + +export const FilterStudentNameInputContainer = styled.div` + width: 100%; +`; + +export const RolesTitle = styled.p` + margin-top: 2%; + margin-right: 2%; + font-size: 1.7vh; +`; + +export const ConfirmsTitle = styled.p` + margin-top: 2%; + margin-right: 2%; + font-size: 1.7vh; +`; + +export const FilterRoles = styled.div` + width: 90%; + display: flex; + align-self: center; + margin-top: 2%; + margin-bottom: 2%; + align-items: center; +`; + +export const FilterConfirms = styled.div` + width: 90%; + display: flex; + align-self: center; + margin-top: 2%; + margin-bottom: 2%; + align-items: center; +`; + +export const FilterRolesDropdownContainer = styled.div` + width: 100%; +`; + +export const FilterConfirmsDropdownContainer = styled.div` + width: 100%; +`; + +export const FilterControls = styled.div` + margin-top: 4%; + flex-direction: column; + display: flex; + align-self: center; + width: 80%; +`; + +export const MessageDiv = styled.div` + text-align: center; + margin-top: 20px; +`; + +export const ConfirmButtonsContainer = styled.div` + display: flex; + flex-direction: column; + align-self: center; + width: 90%; + margin-bottom: 2%; +`; diff --git a/frontend/src/components/StudentsComponents/index.ts b/frontend/src/components/StudentsComponents/index.ts new file mode 100644 index 000000000..945807217 --- /dev/null +++ b/frontend/src/components/StudentsComponents/index.ts @@ -0,0 +1,3 @@ +export { default as StudentCard } from "./StudentList/StudentCard"; +export { default as StudentList } from "./StudentList"; +export { default as StudentListFilters } from "./StudentListFilters"; diff --git a/frontend/src/components/UsersComponents/Coaches/Coaches.tsx b/frontend/src/components/UsersComponents/Coaches/Coaches.tsx index 8581f8e5a..22a94635e 100644 --- a/frontend/src/components/UsersComponents/Coaches/Coaches.tsx +++ b/frontend/src/components/UsersComponents/Coaches/Coaches.tsx @@ -1,21 +1,20 @@ import React from "react"; import { CoachesTitle, CoachesContainer } from "./styles"; import { User } from "../../../utils/api/users/users"; -import { Error, SpinnerContainer } from "../Requests/styles"; import { CoachList, AddCoach } from "./CoachesComponents"; -import { Spinner } from "react-bootstrap"; -import { SearchInput } from "../../styles"; +import { SearchBar } from "../../Common/Forms"; +import { SearchFieldDiv, TableDiv } from "../../Common/Users/styles"; +import LoadSpinner from "../../Common/LoadSpinner"; /** * List of coaches of the given edition. * This includes a searchfield and the option to remove and add coaches. * @param props.edition The edition of which coaches are shown. * @param props.coaches The list of coaches which need to be shown. + * @param props.loading Data is being loaded * @param props.getMoreCoaches A function to load more coaches. * @param props.searchCoaches A function to set the filter for coaches' username. - * @param props.gotData All data is received. - * @param props.gettingData Waiting for data. - * @param props.error An error message. + * @param props.setPage Set the next page to fetch * @param props.moreCoachesAvailable More unfetched coaches available. * @param props.searchTerm Current filter for coaches' names. * @param props.refreshCoaches A function which will be called when a coach is added. @@ -24,11 +23,10 @@ import { SearchInput } from "../../styles"; export default function Coaches(props: { edition: string; coaches: User[]; - getMoreCoaches: (page: number) => void; + loading: boolean; + getMoreCoaches: (page: number, reset: boolean) => void; searchCoaches: (word: string) => void; - gotData: boolean; - gettingData: boolean; - error: string; + setPage: (page: number) => void; moreCoachesAvailable: boolean; searchTerm: string; refreshCoaches: () => void; @@ -36,26 +34,18 @@ export default function Coaches(props: { }) { let table; if (props.coaches.length === 0) { - if (props.gettingData) { - table = ( - - - - ); - } else if (props.gotData) { - table =
    No coaches found
    ; + if (props.loading) { + table = ; } else { - table = {props.error} ; + table =
    No coaches found
    ; } } else { table = ( props.getMoreCoaches(page, false)} moreCoachesAvailable={props.moreCoachesAvailable} /> ); @@ -64,12 +54,18 @@ export default function Coaches(props: { return ( Coaches - props.searchCoaches(e.target.value)} - /> + + { + props.searchCoaches(e.target.value); + props.setPage(0); + }} + value={props.searchTerm} + placeholder="Search name..." + /> + - {table} + {table} ); } diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx index d3b0026e6..26a09e2c8 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx @@ -1,13 +1,18 @@ import { getUsersExcludeEdition, User } from "../../../../utils/api/users/users"; -import React, { useState } from "react"; +import { useState, createRef, useEffect } from "react"; import { addCoachToEdition } from "../../../../utils/api/users/coaches"; import { Button, Modal, Spinner } from "react-bootstrap"; -import { Error } from "../../Requests/styles"; -import { AddAdminButton, ModalContentConfirm } from "../../../AdminsComponents/styles"; +import { AddButtonDiv } from "../../../AdminsComponents/styles"; +import Typeahead from "react-bootstrap-typeahead/types/core/Typeahead"; +import UserMenuItem from "../../../Common/Users/MenuItem"; +import { StyledMenuItem } from "../../../Common/Users/styles"; +import { EmailAndAuth } from "../../../Common/Users"; +import { EmailDiv } from "../styles"; +import CreateButton from "../../../Common/Buttons/CreateButton"; +import { ModalContentConfirm } from "../../../Common/styles"; +import { StyledInput } from "../../../Common/Forms/styles"; import { AsyncTypeahead, Menu } from "react-bootstrap-typeahead"; -import UserMenuItem from "../../../GeneralComponents/MenuItem"; -import { StyledMenuItem } from "../../../GeneralComponents/styles"; -import { EmailAndAuth } from "../../../GeneralComponents"; +import { toast } from "react-toastify"; /** * A button and popup to add a new coach to the given edition. @@ -19,30 +24,37 @@ export default function AddCoach(props: { edition: string; refreshCoaches: () => const [show, setShow] = useState(false); const [selected, setSelected] = useState(undefined); const [loading, setLoading] = useState(false); - const [error, setError] = useState(""); const [gettingData, setGettingData] = useState(false); // Waiting for data const [users, setUsers] = useState([]); // All users which are not a coach const [searchTerm, setSearchTerm] = useState(""); // The word set in filter + const [clearRef, setClearRef] = useState(false); // The ref must be cleared + + const typeaheadRef = createRef(); + + useEffect(() => { + // For some obscure reason the ref can only be cleared in here & not somewhere else + if (clearRef) { + // This triggers itself, but only once, so it doesn't really matter + setClearRef(false); + typeaheadRef.current?.clear(); + } + }, [clearRef, typeaheadRef]); async function getData(page: number, filter: string | undefined = undefined) { if (filter === undefined) { filter = searchTerm; } setGettingData(true); - setError(""); - try { - const response = await getUsersExcludeEdition(props.edition, filter, page); - if (page === 0) { - setUsers(response.users); - } else { - setUsers(users.concat(response.users)); - } - - setGettingData(false); - } catch (exception) { - setError("Oops, something went wrong..."); - setGettingData(false); + const response = await toast.promise(getUsersExcludeEdition(props.edition, filter, page), { + error: "Failed to retrieve users", + }); + if (page === 0) { + setUsers(response.users); + } else { + setUsers(users.concat(response.users)); } + + setGettingData(false); } function filterData(searchTerm: string) { @@ -53,7 +65,7 @@ export default function AddCoach(props: { edition: string; refreshCoaches: () => const handleClose = () => { setSelected(undefined); - setError(""); + props.refreshCoaches(); setShow(false); }; const handleShow = () => { @@ -62,21 +74,15 @@ export default function AddCoach(props: { edition: string; refreshCoaches: () => async function addCoach(user: User) { setLoading(true); - setError(""); - let success = false; - try { - success = await addCoachToEdition(user.userId, props.edition); - if (!success) { - setError("Something went wrong. Failed to add coach"); - } - } catch (error) { - setError("Something went wrong. Failed to add coach"); - } + await toast.promise(addCoachToEdition(user.userId, props.edition), { + error: "Failed to add coach", + pending: "Adding coach", + success: "Coach successfully added", + }); + setLoading(false); - if (success) { - props.refreshCoaches(); - handleClose(); - } + setSelected(undefined); + setClearRef(true); } let addButton; @@ -84,8 +90,8 @@ export default function AddCoach(props: { edition: string; refreshCoaches: () => addButton = ; } else { addButton = ( - + Add coach + ); } return ( <> - - Add coach - + + + Add coach to current edition + + @@ -118,11 +126,20 @@ export default function AddCoach(props: { edition: string; refreshCoaches: () => minLength={1} onSearch={filterData} options={users} - placeholder={"user's name"} + ref={typeaheadRef} + placeholder={"Username"} onChange={selected => { setSelected(selected[0] as User); - setError(""); }} + renderInput={({ inputRef, referenceElementRef, ...inputProps }) => ( + { + inputRef(input); + referenceElementRef(input); + }} + /> + )} renderMenu={(results, menuProps) => { const { newSelectionPrefix, @@ -149,14 +166,15 @@ export default function AddCoach(props: { edition: string; refreshCoaches: () => ); }} /> - + + + {addButton} - {error} diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx index ba28fb538..f6c34d92f 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx @@ -1,26 +1,23 @@ import { User } from "../../../../utils/api/users/users"; -import { SpinnerContainer } from "../../Requests/styles"; -import { Spinner } from "react-bootstrap"; -import { CoachesTable, ListDiv, RemoveTh } from "../styles"; +import { CoachesTable } from "../styles"; import React from "react"; import InfiniteScroll from "react-infinite-scroller"; import { CoachListItem } from "./index"; +import LoadSpinner from "../../../Common/LoadSpinner"; +import { ListDiv } from "../../../Common/Users/styles"; +import { RemoveTh } from "../../../Common/Tables/styles"; /** * A list of [[CoachListItem]]s. * @param props.coaches The list of coaches which needs to be shown. - * @param props.loading Data is not available yet. * @param props.edition The edition. - * @param props.gotData All data is received. * @param props.removeCoach A function which will be called when a coach is removed. * @param props.getMoreCoaches A function to load more coaches. * @param props.moreCoachesAvailable More unfetched coaches available. */ export default function CoachList(props: { coaches: User[]; - loading: boolean; edition: string; - gotData: boolean; removeCoach: (coach: User) => void; getMoreCoaches: (page: number) => void; moreCoachesAvailable: boolean; @@ -28,18 +25,13 @@ export default function CoachList(props: { return ( - - - } + loader={} useWindow={false} initialLoad={true} > - + Name diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx index 0f0b6a094..c3a7ccfdc 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx @@ -1,8 +1,8 @@ import { User } from "../../../../utils/api/users/users"; import React from "react"; import RemoveCoach from "./RemoveCoach"; -import { RemoveTd } from "../styles"; -import { EmailAndAuth } from "../../../GeneralComponents"; +import { EmailAndAuth } from "../../../Common/Users"; +import { RemoveTd } from "../../../Common/Tables/styles"; /** * An item from [[CoachList]] which represents one coach. diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx index c7660e857..52adc63f6 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx @@ -4,9 +4,17 @@ import { removeCoachFromAllEditions, removeCoachFromEdition, } from "../../../../utils/api/users/coaches"; -import { Button, Modal, Spinner } from "react-bootstrap"; -import { DialogButton, ModalContent } from "../styles"; -import { Error } from "../../Requests/styles"; +import { Modal } from "react-bootstrap"; +import { + CancelButton, + CredsDiv, + DialogButtonContainer, + DialogButtonDiv, + ModalContent, +} from "../styles"; +import LoadSpinner from "../../../Common/LoadSpinner"; +import DeleteButton from "../../../Common/Buttons/DeleteButton"; +import { toast } from "react-toastify"; /** * A button (part of [[CoachListItem]]) and popup to remove a user as coach from the given edition or all editions. @@ -21,14 +29,12 @@ export default function RemoveCoach(props: { removeCoach: () => void; }) { const [show, setShow] = useState(false); - const [error, setError] = useState(""); const [loading, setLoading] = useState(false); const handleClose = () => setShow(false); const handleShow = () => { setShow(true); - setError(""); }; /** @@ -38,60 +44,62 @@ export default function RemoveCoach(props: { */ async function removeCoach(userId: number, allEditions: boolean) { setLoading(true); - let removed = false; - try { - if (allEditions) { - removed = await removeCoachFromAllEditions(userId); - } else { - removed = await removeCoachFromEdition(userId, props.edition); - } - - if (removed) { - props.removeCoach(); - } else { - setError("Something went wrong. Failed to remove coach"); - setLoading(false); - } - } catch (error) { - setError("Something went wrong. Failed to remove coach"); - setLoading(false); + if (allEditions) { + await toast.promise(removeCoachFromAllEditions(userId), { + error: "Failed to remove coach", + pending: "Removing coach", + success: "Coach successfully removed", + }); + } else { + await toast.promise(removeCoachFromEdition(userId, props.edition), { + error: "Failed to remove coach", + pending: "Removing coach", + success: "Coach successfully removed", + }); } + + setLoading(false); + props.removeCoach(); } let buttons; if (loading) { - buttons = ; + buttons = ; } else { buttons = ( -
    - { - removeCoach(props.coach.userId, true); - }} - > - Remove from all editions - - { - removeCoach(props.coach.userId, false); - }} - > - Remove from {props.edition} - - -
    + + + { + removeCoach(props.coach.userId, true); + }} + showIcon={false} + > + Remove from all editions + + + + { + removeCoach(props.coach.userId, false); + }} + showIcon={false} + > + Remove from current edition + + + Cancel + + + ); } return ( <> - + @@ -99,13 +107,12 @@ export default function RemoveCoach(props: { Remove Coach -

    {props.coach.name}

    - {props.coach.auth.email} + +

    {props.coach.name}

    + {props.coach.auth.email} +
    - - {buttons} - {error} - + {buttons}
    diff --git a/frontend/src/components/UsersComponents/Coaches/styles.ts b/frontend/src/components/UsersComponents/Coaches/styles.ts index 0d6f27b57..844fc7e6b 100644 --- a/frontend/src/components/UsersComponents/Coaches/styles.ts +++ b/frontend/src/components/UsersComponents/Coaches/styles.ts @@ -2,11 +2,9 @@ import styled from "styled-components"; import { Button, Table } from "react-bootstrap"; export const CoachesContainer = styled.div` - min-width: 450px; - width: 80%; - max-width: 700px; - height: 500px; - margin: 10px auto auto; + width: 50em; + height: fit-content; + margin: 10px auto 20px auto; `; export const CoachesTitle = styled.div` @@ -16,32 +14,36 @@ export const CoachesTitle = styled.div` font-size: 25px; `; -export const CoachesTable = styled(Table)` - // TODO: make all tables in site uniform -`; +export const CoachesTable = styled(Table).attrs({ + striped: true, + bordered: true, + variant: "dark", + hover: false, +})``; export const ModalContent = styled.div` border: 3px solid var(--osoc_red); background-color: var(--osoc_blue); `; -export const RemoveTh = styled.th` - width: 200px; - text-align: center; +export const DialogButtonDiv = styled.div` + margin-right: 4px; + margin-bottom: 4px; `; -export const RemoveTd = styled.td` - text-align: center; - vertical-align: middle; +export const DialogButtonContainer = styled.div` + width: 100%; `; -export const ListDiv = styled.div` - width: 100%; - height: 400px; +export const CancelButton = styled(Button)` + float: right; +`; + +export const EmailDiv = styled.div` overflow: auto; - margin-top: 10px; `; -export const DialogButton = styled(Button)` - margin-right: 4px; +export const CredsDiv = styled.div` + overflow: hidden; + text-overflow: ellipsis; `; diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUser.css b/frontend/src/components/UsersComponents/InviteUser/InviteUser.css deleted file mode 100644 index 2b087a3bc..000000000 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUser.css +++ /dev/null @@ -1,3 +0,0 @@ -.email-field-error { - border: 2px solid red !important; -} diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx index 68b4a09fd..0ec1e6bd9 100644 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx @@ -1,8 +1,9 @@ import React, { useState } from "react"; import { getInviteLink } from "../../../utils/api/users/users"; -import "./InviteUser.css"; -import { InviteInput, InviteContainer, Error, MessageDiv } from "./styles"; +import { InviteContainer, MessageDiv, InputContainer } from "./styles"; import { ButtonsDiv } from "./InviteUserComponents"; +import { SearchBar } from "../../Common/Forms"; +import { toast } from "react-toastify"; /** * A component to invite a user as coach to a given edition. @@ -14,8 +15,6 @@ import { ButtonsDiv } from "./InviteUserComponents"; export default function InviteUser(props: { edition: string }) { const [email, setEmail] = useState(""); // The email address which is entered const [valid, setValid] = useState(true); // The given email address is valid (or still being typed) - const [errorMessage, setErrorMessage] = useState(""); // An error message - const [loading, setLoading] = useState(false); // The invite link is being created const [message, setMessage] = useState(""); // A message to confirm link created /** @@ -26,7 +25,6 @@ export default function InviteUser(props: { edition: string }) { const changeEmail = function (email: string) { setEmail(email); setValid(true); - setErrorMessage(""); setMessage(""); }; @@ -39,26 +37,24 @@ export default function InviteUser(props: { edition: string }) { */ const sendInvite = async (copyInvite: boolean) => { if (/[^@\s]+@[^@\s]+\.[^@\s]+/.test(email)) { - setLoading(true); - try { - const response = await getInviteLink(props.edition, email); - if (copyInvite) { - await navigator.clipboard.writeText(response.inviteLink); - setMessage("Copied invite link for " + email); - } else { - window.open(response.mailTo); - setMessage("Created email for " + email); - } - setLoading(false); - setEmail(""); - } catch (error) { - setLoading(false); - setErrorMessage("Something went wrong"); - setMessage(""); + const response = await toast.promise(getInviteLink(props.edition, email), { + error: "Failed to create invite", + pending: "Creating invite", + success: "Invite successfully created", + }); + if (copyInvite) { + await navigator.clipboard.writeText(response.inviteLink); + setMessage("Copied invite link for " + email); + } else { + window.open(response.mailTo); + setMessage("Created email for " + email); } + setEmail(""); } else { setValid(false); - setErrorMessage("Invalid email"); + toast.error("Invalid email address", { + toastId: "invalid_email", + }); setMessage(""); } }; @@ -66,17 +62,17 @@ export default function InviteUser(props: { edition: string }) { return (
    - changeEmail(e.target.value)} - /> - + + changeEmail(e.target.value)} + isInvalid={!valid} + placeholder="Email address" + /> + + - - {message} - {errorMessage} - + {message}
    ); } diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/SendInviteButton.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/SendInviteButton.tsx index 97a8f3030..aa65b5404 100644 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/SendInviteButton.tsx +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/SendInviteButton.tsx @@ -1,27 +1,22 @@ import { DropdownField, InviteButton } from "../styles"; import React from "react"; -import { Button, ButtonGroup, Dropdown, Spinner } from "react-bootstrap"; +import { ButtonGroup, Dropdown } from "react-bootstrap"; +import { CreateButton } from "../../../Common/Buttons"; +import { DropdownToggle } from "../../../Common/Buttons/styles"; /** * A component to choice between sending an invite or copying it to clipboard. - * @param props.loading Invite is being created. Used to show a spinner. * @param props.sendInvite A function to send/copy the link. */ -export default function SendInviteButton(props: { - loading: boolean; - sendInvite: (copy: boolean) => void; -}) { - if (props.loading) { - return ; - } +export default function SendInviteButton(props: { sendInvite: (copy: boolean) => void }) { return ( - - + - + props.sendInvite(true)}> diff --git a/frontend/src/components/UsersComponents/InviteUser/styles.ts b/frontend/src/components/UsersComponents/InviteUser/styles.ts index d264261f6..b5fd842df 100644 --- a/frontend/src/components/UsersComponents/InviteUser/styles.ts +++ b/frontend/src/components/UsersComponents/InviteUser/styles.ts @@ -24,7 +24,8 @@ export const InviteInput = styled.input.attrs({ export const MessageDiv = styled.div` margin-left: 10px; margin-top: 5px; - height: 15px; + height: fit-content; + overflow: auto; `; export const Error = styled.div` @@ -45,3 +46,11 @@ export const DropdownField = styled(Dropdown.Item)` transition: 200ms ease-in; } `; + +export const InputContainer = styled.div` + width: 15em; + float: left; + margin-top: 10px; + margin-left: 10px; + margin-right: 5px; +`; diff --git a/frontend/src/components/UsersComponents/Requests/Requests.tsx b/frontend/src/components/UsersComponents/Requests/Requests.tsx index 59f201a3b..36daa975b 100644 --- a/frontend/src/components/UsersComponents/Requests/Requests.tsx +++ b/frontend/src/components/UsersComponents/Requests/Requests.tsx @@ -1,10 +1,12 @@ import React, { useEffect, useState } from "react"; import Collapsible from "react-collapsible"; -import { RequestsContainer, Error, SpinnerContainer, RequestListContainer } from "./styles"; +import { RequestsContainer, RequestListContainer } from "./styles"; import { getRequests, Request } from "../../../utils/api/users/requests"; import { RequestList, RequestsHeader } from "./RequestsComponents"; -import { Spinner } from "react-bootstrap"; -import { SearchInput } from "../../styles"; +import SearchBar from "../../Common/Forms/SearchBar"; +import { SearchFieldDiv } from "../../Common/Users/styles"; +import { toast } from "react-toastify"; +import { LoadSpinner } from "../../Common"; /** * A collapsible component which contains all coach requests for a given edition. @@ -13,13 +15,17 @@ import { SearchInput } from "../../styles"; * @param props.refreshCoaches A function which will be called when a new coach is added */ export default function Requests(props: { edition: string; refreshCoaches: () => void }) { + const [allRequests, setAllRequests] = useState([]); const [requests, setRequests] = useState([]); // All requests after filter - const [gettingRequests, setGettingRequests] = useState(false); // Waiting for data + const [loading, setLoading] = useState(false); // Waiting for data const [searchTerm, setSearchTerm] = useState(""); // The word set in the filter - const [gotData, setGotData] = useState(false); // Received data + const [requestedEdition, setRequestedEdition] = useState(props.edition); const [open, setOpen] = useState(false); // Collapsible is open - const [error, setError] = useState(""); // Error message const [moreRequestsAvailable, setMoreRequestsAvailable] = useState(true); // Endpoint has more requests available + const [allRequestsFetched, setAllRequestsFetched] = useState(false); + const [page, setPage] = useState(0); // The next page which needs to be fetched + + const [controller, setController] = useState(undefined); /** * Remove a request from the list of requests (Request is accepter or rejected). @@ -33,75 +39,105 @@ export default function Requests(props: { edition: string; refreshCoaches: () => return object !== request; }) ); + setAllRequests( + allRequests.filter(object => { + return object !== request; + }) + ); if (accepted) { props.refreshCoaches(); } } /** - * Request a page from the list of requests. - * An optional filter can be used to filter the username. - * If the filter is not used, the string saved in the "searchTerm" state will be used. - * @param page The page to load. - * @param filter Optional string to filter username. + * Request the next page from the list of requests. + * The set searchterm will be used. */ - async function getData(page: number, filter: string | undefined = undefined) { - if (filter === undefined) { - filter = searchTerm; + async function getData(requested: number, reset: boolean) { + const filterChanged = requested === -1; + const requestedPage = requested === -1 ? 0 : page; + + if (loading && !filterChanged) { + return; } - setGettingRequests(true); - setError(""); - try { - const response = await getRequests(props.edition, filter, page); - if (response.requests.length === 0) { + + if (allRequestsFetched && !reset) { + setRequests( + allRequests.filter(request => + request.user.name.toUpperCase().includes(searchTerm.toUpperCase()) + ) + ); + setMoreRequestsAvailable(false); + return; + } + + setLoading(true); + + if (controller !== undefined) { + controller.abort(); + } + const newController = new AbortController(); + setController(newController); + + const response = await toast.promise( + getRequests(props.edition, searchTerm, requestedPage, newController), + { + error: "Failed to retrieve requests", + } + ); + + if (response !== null) { + if (response.requests.length === 0 && !filterChanged) { setMoreRequestsAvailable(false); + } else { + setMoreRequestsAvailable(true); } - if (page === 0) { + if (requestedPage === 0 || filterChanged) { setRequests(response.requests); } else { setRequests(requests.concat(response.requests)); } - setGotData(true); - setGettingRequests(false); - } catch (exception) { - setError("Oops, something went wrong..."); - setGettingRequests(false); + if (searchTerm === "") { + if (response.requests.length === 0 && !filterChanged) { + setAllRequestsFetched(true); + } + if (requestedPage === 0) { + setAllRequests(response.requests); + } else { + setAllRequests(allRequests.concat(response.requests)); + } + } + + setPage(requestedPage + 1); + } else { + setMoreRequestsAvailable(false); } + setLoading(false); } useEffect(() => { - if (!gotData && !gettingRequests && !error) { - getData(0); + if (props.edition !== requestedEdition) { + setRequests([]); + setPage(0); + setAllRequestsFetched(false); + setMoreRequestsAvailable(true); + getData(-1, true); + setRequestedEdition(props.edition); + } else { + setPage(0); + setMoreRequestsAvailable(true); + getData(-1, false); } - }); - - /** - * Set the searchTerm and request the first page with this filter. - * The current list of requests will be resetted. - * @param searchTerm The string to filter coaches with by username. - */ - function filterRequests(searchTerm: string) { - setGotData(false); - setSearchTerm(searchTerm); - setRequests([]); - setMoreRequestsAvailable(true); - getData(0, searchTerm); - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchTerm, props.edition]); let list; if (requests.length === 0) { - if (gettingRequests) { - list = ( - - - - ); - } else if (gotData) { - list =
    No requests found
    ; - } else { - list = {error}; + if (loading) { + list = ; } + list =
    No requests found
    ; } else { list = ( onOpening={() => setOpen(true)} onClosing={() => setOpen(false)} > - filterRequests(e.target.value)} /> + + { + setPage(0); + setSearchTerm(e.target.value); + }} + value={searchTerm} + placeholder="Search name..." + /> + {list} diff --git a/frontend/src/components/UsersComponents/Requests/RequestsComponents/AcceptReject.tsx b/frontend/src/components/UsersComponents/Requests/RequestsComponents/AcceptReject.tsx index 5284ecf26..9e7e095dc 100644 --- a/frontend/src/components/UsersComponents/Requests/RequestsComponents/AcceptReject.tsx +++ b/frontend/src/components/UsersComponents/Requests/RequestsComponents/AcceptReject.tsx @@ -1,7 +1,9 @@ -import { AcceptButton, RejectButton } from "../styles"; import { Request, acceptRequest, rejectRequest } from "../../../../utils/api/users/requests"; -import React, { useState } from "react"; -import { Spinner } from "react-bootstrap"; +import React from "react"; +import CreateButton from "../../../Common/Buttons/CreateButton"; +import DeleteButton from "../../../Common/Buttons/DeleteButton"; +import { Spacing } from "../styles"; +import { toast } from "react-toastify"; /** * Component consisting of two buttons to accept or reject a coach request. @@ -12,55 +14,33 @@ export default function AcceptReject(props: { request: Request; removeRequest: (coachAdded: boolean, request: Request) => void; }) { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(""); - async function accept() { - setLoading(true); - let success = false; - try { - success = await acceptRequest(props.request.requestId); - if (!success) { - setError("Failed to accept"); - } - } catch (exception) { - setError("Failed to accept"); - } - setLoading(false); - if (success) { - props.removeRequest(true, props.request); - } - } + await toast.promise(acceptRequest(props.request.requestId), { + error: "Failed to accept request", + pending: "Accepting request", + }); - async function reject() { - setLoading(true); - let success = false; - try { - success = await rejectRequest(props.request.requestId); - if (!success) { - setError("Failed to reject"); - } - } catch (exception) { - setError("Failed to reject"); - } - setLoading(false); - if (success) { - props.removeRequest(false, props.request); - } + props.removeRequest(true, props.request); } - if (error) { - return
    {error}
    ; - } + async function reject() { + await toast.promise(rejectRequest(props.request.requestId), { + error: "Failed to reject request", + pending: "Rejecting request", + }); - if (loading) { - return ; + props.removeRequest(false, props.request); } return (
    - Accept - Reject + + Accept + + + + Reject +
    ); } diff --git a/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestList.tsx b/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestList.tsx index f85315cb9..beb03a4f6 100644 --- a/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestList.tsx +++ b/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestList.tsx @@ -1,10 +1,10 @@ import { Request } from "../../../../utils/api/users/requests"; -import { AcceptRejectTh, RequestsTable, SpinnerContainer } from "../styles"; -import { Spinner } from "react-bootstrap"; +import { AcceptRejectTh, RequestsTable } from "../styles"; import React from "react"; import RequestListItem from "./RequestListItem"; import InfiniteScroll from "react-infinite-scroller"; -import { ListDiv } from "../../Coaches/styles"; +import LoadSpinner from "../../../Common/LoadSpinner"; +import { ListDiv } from "../../../Common/Users/styles"; /** * A list of [[RequestListItem]]s. @@ -17,23 +17,18 @@ export default function RequestList(props: { requests: Request[]; removeRequest: (coachAdded: boolean, request: Request) => void; moreRequestsAvailable: boolean; - getMoreRequests: (page: number) => void; + getMoreRequests: (page: number, reset: boolean) => void; }) { return ( props.getMoreRequests(page, false)} hasMore={props.moreRequestsAvailable} - loader={ - - - - } + loader={} useWindow={false} initialLoad={true} > - + Name diff --git a/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestListItem.tsx b/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestListItem.tsx index a7889d036..d70917509 100644 --- a/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestListItem.tsx +++ b/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestListItem.tsx @@ -2,7 +2,7 @@ import { Request } from "../../../../utils/api/users/requests"; import React from "react"; import AcceptReject from "./AcceptReject"; import { AcceptRejectTd } from "../styles"; -import { EmailAndAuth } from "../../../GeneralComponents"; +import { EmailAndAuth } from "../../../Common/Users"; /** * An item from [[RequestList]] which represents one request. diff --git a/frontend/src/components/UsersComponents/Requests/styles.ts b/frontend/src/components/UsersComponents/Requests/styles.ts index a0bf15341..b2899fa20 100644 --- a/frontend/src/components/UsersComponents/Requests/styles.ts +++ b/frontend/src/components/UsersComponents/Requests/styles.ts @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { Table, Button } from "react-bootstrap"; +import { Table } from "react-bootstrap"; import { BiDownArrow } from "react-icons/bi"; export const RequestHeaderDiv = styled.div` @@ -27,14 +27,16 @@ export const ClosedArrow = styled(BiDownArrow)` offset: 0 30px; `; -export const RequestsTable = styled(Table)` - // TODO: make all tables in site uniform -`; +export const RequestsTable = styled(Table).attrs({ + striped: true, + bordered: true, + variant: "dark", + hover: false, +})``; export const RequestsContainer = styled.div` - min-width: 450px; - width: 80%; - max-width: 700px; + width: 50em; + height: fit-content; margin: 10px auto auto; `; @@ -48,38 +50,13 @@ export const AcceptRejectTd = styled.td` vertical-align: middle; `; -export const AcceptButton = styled(Button)` - background-color: var(--osoc_green); - color: black; - padding-bottom: 3px; - padding-left: 3px; - padding-right: 3px; - width: 65px; -`; - -export const RejectButton = styled(Button)` - background-color: var(--osoc_red); - color: black; - margin-left: 3px; - padding-bottom: 3px; - padding-left: 3px; - padding-right: 3px; - width: 65px; -`; - -export const SpinnerContainer = styled.div` - display: flex; - justify-content: center; - align-items: center; - margin: 20px; -`; - -export const Error = styled.div` - color: var(--osoc_red); - width: 100%; - margin: auto; +export const Spacing = styled.div` + display: inline-block; + width: 5px; `; export const RequestListContainer = styled.div` - height: 400px; + height: fit-content; + width: 100%; + clear: left; `; diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index bd4445e73..ed42f24e2 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -1,4 +1,6 @@ export { default as AdminRoute } from "./AdminRoute"; +export { default as CurrentEditionRoute } from "./CurrentEditionRoute"; +export * as Common from "./Common"; export { default as Footer } from "./Footer"; export * as LoginComponents from "./LoginComponents"; export { default as Navbar } from "./Navbar"; @@ -8,4 +10,5 @@ export * as RegisterComponents from "./RegisterComponents"; export { default as Modal } from "./ProjectsComponents/ConfirmDelete"; export * as UsersComponents from "./UsersComponents"; export * as AdminsComponents from "./AdminsComponents"; -export * as GeneralComponents from "./GeneralComponents"; +export * as GeneralComponents from "./Common/Users"; +export * as ProjectsComponenets from "./ProjectsComponents"; diff --git a/frontend/src/components/styles.ts b/frontend/src/components/styles.ts index f7091348b..9c59a4944 100644 --- a/frontend/src/components/styles.ts +++ b/frontend/src/components/styles.ts @@ -3,9 +3,9 @@ import styled from "styled-components"; export const SearchInput = styled.input.attrs({ placeholder: "Search", })` - margin: 3px; width: 150px; font-size: 15px; border-radius: 5px; - border-width: 0; + border: none; + height: 28px; `; diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx index bfd2a818d..5e7ccc46f 100644 --- a/frontend/src/contexts/auth-context.tsx +++ b/frontend/src/contexts/auth-context.tsx @@ -1,7 +1,7 @@ /** Context hook to maintain the authentication state of the user **/ import { Role } from "../data/enums"; import React, { useContext, ReactNode, useState } from "react"; -import { User } from "../data/interfaces"; +import { Edition, User } from "../data/interfaces"; import { setCurrentEdition } from "../utils/session-storage"; import { setAccessToken, setRefreshToken } from "../utils/local-storage"; @@ -15,8 +15,8 @@ export interface AuthContextState { setRole: (value: Role | null) => void; userId: number | null; setUserId: (value: number | null) => void; - editions: string[]; - setEditions: (value: string[]) => void; + editions: Edition[]; + setEditions: (value: Edition[]) => void; } /** @@ -33,7 +33,7 @@ function authDefaultState(): AuthContextState { userId: null, setUserId: (_: number | null) => {}, editions: [], - setEditions: (_: string[]) => {}, + setEditions: (_: Edition[]) => {}, }; } @@ -56,7 +56,7 @@ export function useAuth(): AuthContextState { export function AuthProvider({ children }: { children: ReactNode }) { const [isLoggedIn, setIsLoggedIn] = useState(null); const [role, setRole] = useState(null); - const [editions, setEditions] = useState([]); + const [editions, setEditions] = useState([]); const [userId, setUserId] = useState(null); // Create AuthContext value @@ -100,3 +100,18 @@ export function logOut(authContext: AuthContextState) { // Remove current edition from SessionStorage setCurrentEdition(null); } + +/** + * Update the state of an edition in the AuthContext + */ +export function updateEditionState(authContext: AuthContextState, edition: Edition) { + const index = authContext.editions.findIndex(e => e.name === edition.name); + if (index === -1) return; + + // Flip the state of the element + const copy = [...authContext.editions]; + copy[index].readonly = !copy[index].readonly; + + // Call the setter to update the state + authContext.setEditions(copy); +} diff --git a/frontend/src/contexts/index.ts b/frontend/src/contexts/index.ts index 133c24ae4..7077b319f 100644 --- a/frontend/src/contexts/index.ts +++ b/frontend/src/contexts/index.ts @@ -1,3 +1,4 @@ import type { AuthContextState } from "./auth-context"; export type { AuthContextState }; -export { AuthProvider, logIn, logOut, useAuth } from "./auth-context"; +export { AuthProvider, logIn, logOut, useAuth, updateEditionState } from "./auth-context"; +export { SocketProvider, useSockets } from "./socket-context"; diff --git a/frontend/src/contexts/socket-context.tsx b/frontend/src/contexts/socket-context.tsx new file mode 100644 index 000000000..2060499c8 --- /dev/null +++ b/frontend/src/contexts/socket-context.tsx @@ -0,0 +1,75 @@ +import React, { ReactNode, useContext, useState } from "react"; +import { BE_DOMAIN, secure } from "../settings"; +import { getAccessToken } from "../utils/local-storage"; + +export interface SocketState { + edition: string | null; + setEdition: (value: string | null) => void; + socket: WebSocket | null; + setSocket: (socket: WebSocket | null) => void; + ensureSocket: (to: string | null) => void; +} + +function socketDefaultState(): SocketState { + return { + edition: null, + setEdition: (_: string | null) => {}, + socket: null, + setSocket: (_: WebSocket | null) => {}, + ensureSocket: (_: string | null) => {}, + }; +} + +export const SocketContext = React.createContext(socketDefaultState()); + +export function useSockets(): SocketState { + return useContext(SocketContext); +} + +function createSocketUrl(edition: string): string { + const token = getAccessToken(); + return `ws${secure}://${BE_DOMAIN}/editions/${edition}/live?token=${token}`; +} + +export function SocketProvider({ children }: { children: ReactNode }) { + const [edition, setEdition] = useState(null); + const [socket, setSocket] = useState(null); + + function ensureSocket(to: string | null) { + // If the destination is empty, close any connections that may be open + // This could mean that a coach was removed from their edition + // so they should no longer be receiving events + if (to === null) { + if (socket !== null) { + socket.close(); + } + + return; + } + + // Currently connected to the required edition + if (edition === to) { + return; + } + + // Already connected to another edition, close the connection first + if (socket !== null) { + socket.close(); + } + + // Create a websocket connection to the requested edition + setSocket(new WebSocket(createSocketUrl(to))); + // Save the new edition as the currently-connected edition + setEdition(to); + } + + const contextValue: SocketState = { + edition: edition, + setEdition: setEdition, + socket: socket, + setSocket: setSocket, + ensureSocket: ensureSocket, + }; + + return {children}; +} diff --git a/frontend/src/data/enums/authType.ts b/frontend/src/data/enums/authType.ts index 53e5ffd73..efa8f7602 100644 --- a/frontend/src/data/enums/authType.ts +++ b/frontend/src/data/enums/authType.ts @@ -4,5 +4,4 @@ export enum AuthType { Email = "email", GitHub = "github", - Google = "google", } diff --git a/frontend/src/data/enums/emailtype.ts b/frontend/src/data/enums/emailtype.ts new file mode 100644 index 000000000..ede29357b --- /dev/null +++ b/frontend/src/data/enums/emailtype.ts @@ -0,0 +1,17 @@ +/** + * Enum for the different types of emails that can be sent + */ +export enum EmailType { + // Nothing happened (undecided/screening) + APPLIED = "Applied", + // We're looking for a project (maybe) + AWAITING_PROJECT = "Awaiting Project", + // Can participate (yes) + APPROVED = "Approved", + // Student signed the contract + CONTRACT_CONFIRMED = "Contract Confirmed", + // Student indicated they don't want to participate anymore + CONTRACT_DECLINED = "Contract Declined", + // We've rejected the student ourselves (no) + REJECTED = "Rejected", +} diff --git a/frontend/src/data/enums/index.ts b/frontend/src/data/enums/index.ts index e9e2bc5f0..429c6f65b 100644 --- a/frontend/src/data/enums/index.ts +++ b/frontend/src/data/enums/index.ts @@ -1,4 +1,6 @@ +export { OAuthProvider } from "./oauth-provider"; export { LocalStorageKey } from "./local-storage"; export { SessionStorageKey } from "./session-storage"; export { Role } from "./role"; +export { EmailType } from "./emailtype"; export { AuthType } from "./authType"; diff --git a/frontend/src/data/enums/oauth-provider.ts b/frontend/src/data/enums/oauth-provider.ts new file mode 100644 index 000000000..75e6ebb09 --- /dev/null +++ b/frontend/src/data/enums/oauth-provider.ts @@ -0,0 +1,7 @@ +/** + * Enum for supported OAuth providers + */ +export const enum OAuthProvider { + GITHUB = "github", + GOOGLE = "google", +} diff --git a/frontend/src/data/enums/session-storage.ts b/frontend/src/data/enums/session-storage.ts index 2eb625515..592f3aab4 100644 --- a/frontend/src/data/enums/session-storage.ts +++ b/frontend/src/data/enums/session-storage.ts @@ -3,4 +3,14 @@ */ export const enum SessionStorageKey { CURRENT_EDITION = "currentEdition", + REGISTER_STATE = "registerState", + /** + * Enums used for storing filters + */ + SUGGESTED_FILTER = "suggestedFilter", + ALUMNI_FILTER = "alumniFilter", + NAME_FILTER = "nameFilter", + STUDENT_COACH_VOLUNTEER_FILTER = "studentCoachVolunteerFilter", + ROLES_FILTER = "rolesFilter", + CONFIRM_FILTER = "confirmFilter", } diff --git a/frontend/src/data/enums/suggestions.ts b/frontend/src/data/enums/suggestions.ts new file mode 100644 index 000000000..22cb32c65 --- /dev/null +++ b/frontend/src/data/enums/suggestions.ts @@ -0,0 +1,14 @@ +/** + * Enum for suggestions + */ +export const enum SuggestionEnum { + YES = 1, + MAYBE = 2, + NO = 3, +} + +export const enum SuggestionColor { + YES = "#44dba4", + MAYBE = "#fcb70f", + NO = "#f14a3b", +} diff --git a/frontend/src/data/interfaces/conflicts.ts b/frontend/src/data/interfaces/conflicts.ts new file mode 100644 index 000000000..7a5f7f47b --- /dev/null +++ b/frontend/src/data/interfaces/conflicts.ts @@ -0,0 +1,21 @@ +export interface PrSuggestion { + projectRole: { project: { name: string; projectId: number } }; + projectRoleSuggestionId: number; +} + +/** + * A conflict (student with multiple projects) + */ +export interface Conflict { + firstName: string; + lastName: string; + prSuggestions: PrSuggestion[]; + studentId: number; +} + +/** + * A list of conflicts + */ +export interface Conflicts { + conflictStudents: Conflict[]; +} diff --git a/frontend/src/data/interfaces/editions.ts b/frontend/src/data/interfaces/editions.ts index db1e88e65..6fbebd074 100644 --- a/frontend/src/data/interfaces/editions.ts +++ b/frontend/src/data/interfaces/editions.ts @@ -4,4 +4,5 @@ export interface Edition { name: string; year: number; + readonly: boolean; } diff --git a/frontend/src/data/interfaces/email.ts b/frontend/src/data/interfaces/email.ts new file mode 100644 index 000000000..7e9e8311d --- /dev/null +++ b/frontend/src/data/interfaces/email.ts @@ -0,0 +1,18 @@ +/** + * A sent email + */ +import { Student } from "./students"; + +export interface Email { + emailId: number; + studentId: number; + decision: number; + date: String; +} +/** + * A list of sent emails + */ +export interface EmailHistoryList { + emails: Email[]; + student: Student; +} diff --git a/frontend/src/data/interfaces/index.ts b/frontend/src/data/interfaces/index.ts index 61fd40869..65bf2e722 100644 --- a/frontend/src/data/interfaces/index.ts +++ b/frontend/src/data/interfaces/index.ts @@ -1,3 +1,6 @@ export type { Edition } from "./editions"; export type { User } from "./users"; -export type { Partner, Coach, Project } from "./projects"; +export type { Partner, Coach, Project, CreateProject } from "./projects"; +export type { Email, EmailHistoryList } from "./email"; +export type { Student } from "./student"; +export type { Conflicts, Conflict, PrSuggestion } from "./conflicts"; diff --git a/frontend/src/data/interfaces/projects.ts b/frontend/src/data/interfaces/projects.ts index 6609d8574..90815823a 100644 --- a/frontend/src/data/interfaces/projects.ts +++ b/frontend/src/data/interfaces/projects.ts @@ -1,7 +1,10 @@ /** - * This file contains all interfaces used in projects pages. + * This file contains all interfaces used for projects. */ +import { Skill } from "./skills"; +import { Student } from "./students"; + /** * Data about a partner. */ @@ -14,11 +17,69 @@ export interface Partner { * Data about a coach. */ export interface Coach { + /** The user's ID */ + userId: number; /** The name of the coach */ name: string; +} - /** The user's ID */ - userId: number; +/** + * Data about a single project role suggestion. + */ +export interface ProjectRoleSuggestion { + /** The id of the suggestion */ + projectRoleSuggestionId: number; + + /** The argumentation why this student is a good fit */ + argumentation: string; + + /** The user who suggested this student */ + drafter: Coach; + + /** The suggested student */ + student: Student; +} + +/** + * Data to create a new project role suggestion. + */ +export interface AddRoleSuggestion { + /** The argumentation why this student is a good fit */ + argumentation: string; +} + +export interface AddStudentRole { + /** The Id of the project role where to add the student */ + projectRoleId: string; + + /** The Id of the student to add */ + studentId: string; + + /** Can be used to switch the role of a student */ + switchProjectRoleId: string | undefined; +} + +/** + * Data about a project role + */ +export interface ProjectRole { + /** The id of the project role */ + projectRoleId: number; + + /** The id of the project this role belongs to */ + projectId: number; + + /** More info about the skill */ + description: string; + + /** The skill needed for this role */ + skill: Skill; + + /** The number of positions this role has */ + slots: number; + + /** The suggested students for this role */ + suggestions: ProjectRoleSuggestion[]; } /** @@ -26,27 +87,27 @@ export interface Coach { * Such as a list of the partners and the coaches */ export interface Project { + /** The project's ID */ + projectId: number; + /** The name of the project */ name: string; - /** How many students are needed for this project */ - numberOfStudents: number; - - /** The partners of this project */ - partners: Partner[]; + /** An url with more info */ + infoUrl: string | null; /** The coaches of this project */ coaches: Coach[]; - /** The name of the edition this project belongs to */ - editionName: string; + /** The partners of this project */ + partners: Partner[]; - /** The project's ID */ - projectId: number; + /** The roles of this project */ + projectRoles: ProjectRole[]; } /** - * Used as an response object for multiple projects + * Used as a response object for multiple projects */ export interface Projects { /** A list of projects */ @@ -58,13 +119,13 @@ export interface Projects { */ export interface SkillProject { /** The name of the skill */ - skill: string; + skill: Skill; /** More info about this skill in a specific project */ description: string; /** Number of positions of this skill in a project */ - amount: number; + slots: number; } /** @@ -74,11 +135,8 @@ export interface CreateProject { /** The name of the new project */ name: string; - /** Number of students the project needs */ - number_of_students: number; - - /** The required skills for the project */ - skills: string[]; + /** An url with more info */ + info_url: string | null; /** The partners that belong to this project */ partners: string[]; @@ -87,16 +145,13 @@ export interface CreateProject { coaches: number[]; } -/** - * Data about a place in a project - */ -export interface StudentPlace { - /** Whether or not this position is filled in */ - available: boolean; +export interface CreateProjectRole { + /** The id of the skill */ + skill_id: number; - /** The skill needed for this place */ - skill: string; + /** More info about this skill in a specific project */ + description: string; - /** The name of the student if this place is filled in */ - name: string | undefined; + /** Number of positions of this skill in a project */ + slots: number; } diff --git a/frontend/src/data/interfaces/questions.ts b/frontend/src/data/interfaces/questions.ts new file mode 100644 index 000000000..e836e4ee8 --- /dev/null +++ b/frontend/src/data/interfaces/questions.ts @@ -0,0 +1,15 @@ +export interface Question { + question: string; + answers: string[]; + files: StudentFile[]; +} + +export interface StudentFile { + filename: string; + mimeType: string; + url: string; +} + +export interface Questions { + qAndA: Question[]; +} diff --git a/frontend/src/data/interfaces/skills.ts b/frontend/src/data/interfaces/skills.ts new file mode 100644 index 000000000..afbb6ff3f --- /dev/null +++ b/frontend/src/data/interfaces/skills.ts @@ -0,0 +1,26 @@ +/** + * Data about a skill + */ +export interface Skill { + /** The id of the skill */ + skillId: number; + + /** The name of the skill */ + name: string; +} + +/** + * To create a skill + */ +export interface CreateSkill { + /** The name of the skill */ + name: string; +} + +/** + * A list of skills + */ +export interface Skills { + /** The list of skills */ + skills: Skill[]; +} diff --git a/frontend/src/data/interfaces/student.ts b/frontend/src/data/interfaces/student.ts new file mode 100644 index 000000000..18b016e72 --- /dev/null +++ b/frontend/src/data/interfaces/student.ts @@ -0,0 +1,8 @@ +/** + * Data about a student in the application + */ +export interface Student { + studentId: number; + firstName: string; + lastName: string; +} diff --git a/frontend/src/data/interfaces/students.ts b/frontend/src/data/interfaces/students.ts new file mode 100644 index 000000000..703368b6b --- /dev/null +++ b/frontend/src/data/interfaces/students.ts @@ -0,0 +1,40 @@ +/** + * This file contains all interfaces used in students pages. + */ + +import { Skill } from "./skills"; + +/** + * Data about a student. + */ +export interface Student { + alumni: boolean; + editionId: number; + emailAddress: string; + finalDecision: number; + firstName: string; + lastName: string; + nrOfSuggestions: NrSuggestions; + phoneNumber: string; + preferredName?: string; + skills: Skill[]; + studentId: number; + wantsToBeStudentCoach: boolean; +} + +/** + * Used as a response object for multiple students. + */ +export interface Students { + /** A list of students */ + students: Student[]; +} + +/** + * Data to represent the amount of suggestions for each suggestion. + */ +export interface NrSuggestions { + yes: number; + maybe: number; + no: number; +} diff --git a/frontend/src/data/interfaces/suggestions.ts b/frontend/src/data/interfaces/suggestions.ts new file mode 100644 index 000000000..33a0eaf1a --- /dev/null +++ b/frontend/src/data/interfaces/suggestions.ts @@ -0,0 +1,22 @@ +export interface Suggestion { + suggestionId: number; + coach: OsocCoach; + suggestion: number; + argumentation: string; +} + +export interface OsocCoach { + userId: number; + name: string; + admin: boolean; + auth: Authentication; +} + +export interface Authentication { + authType: string; + email: string; +} + +export interface Suggestions { + suggestions: Suggestion[]; +} diff --git a/frontend/src/data/interfaces/users.ts b/frontend/src/data/interfaces/users.ts index 1c653263d..7622c7f0e 100644 --- a/frontend/src/data/interfaces/users.ts +++ b/frontend/src/data/interfaces/users.ts @@ -1,3 +1,5 @@ +import { Edition } from "./editions"; + /** * Data about a user using the application. * Contains a list of edition names so that we can quickly check if @@ -7,5 +9,5 @@ export interface User { userId: number; name: string; admin: boolean; - editions: string[]; + editions: Edition[]; } diff --git a/frontend/src/data/interfaces/websockets.ts b/frontend/src/data/interfaces/websockets.ts new file mode 100644 index 000000000..27e4d75c4 --- /dev/null +++ b/frontend/src/data/interfaces/websockets.ts @@ -0,0 +1,33 @@ +/** + * Enum for the types of events sent over a websocket + */ +export enum EventType { + PROJECT, + PROJECT_ROLE, + PROJECT_ROLE_SUGGESTION, + STUDENT, + STUDENT_SUGGESTION, +} + +/** + * Enum for the request method used when triggering this websocket event + */ +export enum RequestMethod { + DELETE = "DELETE", + GET = "GET", + PATCH = "PATCH", + POST = "POST", +} + +/** + * Basic interface for an event sent over a websocket + */ +export interface WebSocketEvent { + method: RequestMethod; + pathIds: { + projectId?: string; + studentId?: string; + suggestionId?: string; + }; + eventType: EventType; +} diff --git a/frontend/src/environment.d.ts b/frontend/src/environment.d.ts index 13f60b8ba..ea9203fd0 100644 --- a/frontend/src/environment.d.ts +++ b/frontend/src/environment.d.ts @@ -2,7 +2,9 @@ declare global { namespace NodeJS { interface ProcessEnv { - REACT_APP_BASE_URL: string; + REACT_APP_BE_DOMAIN: string; + REACT_APP_FE_BASE_URL: string; + REACT_APP_GITHUB_CLIENT_ID: string; } } } diff --git a/frontend/src/images/letters/osoc_c.svg b/frontend/src/images/letters/osoc_c.svg index e2a0e212a..5d129246d 100644 --- a/frontend/src/images/letters/osoc_c.svg +++ b/frontend/src/images/letters/osoc_c.svg @@ -1,15 +1,15 @@ - - - - - - - - + + + + + + + + diff --git a/frontend/src/settings.ts b/frontend/src/settings.ts index 88e9e9733..2d7e759b6 100644 --- a/frontend/src/settings.ts +++ b/frontend/src/settings.ts @@ -1 +1,6 @@ -export const BASE_URL: string = process.env.REACT_APP_BASE_URL || "http://localhost:8000"; +export const BE_DOMAIN: string = process.env.REACT_APP_BE_DOMAIN || "localhost:8000"; +export const secure = BE_DOMAIN.startsWith("localhost") ? "" : "s"; +export const BE_BASE_URL: string = `http${secure}://${BE_DOMAIN}`; // HTTPS if not localhost +export const FE_BASE_URL: string = process.env.REACT_APP_FE_BASE_URL || "http://localhost:3000"; +export const GITHUB_CLIENT_ID: string = + process.env.REACT_APP_GITHUB_CLIENT_ID || "create_an_env_variable"; diff --git a/frontend/src/utils/api/api.ts b/frontend/src/utils/api/api.ts index 8c48e6af0..24ede4e55 100644 --- a/frontend/src/utils/api/api.ts +++ b/frontend/src/utils/api/api.ts @@ -1,5 +1,5 @@ import axios, { AxiosError } from "axios"; -import { BASE_URL } from "../../settings"; +import { BE_BASE_URL } from "../../settings"; import { getAccessToken, getRefreshTokenLock, @@ -8,10 +8,11 @@ import { setRefreshTokenLock, } from "../local-storage/auth"; import { refreshTokens } from "./auth"; +import { isRegisterPath } from "../logic"; export const axiosInstance = axios.create(); -axiosInstance.defaults.baseURL = BASE_URL; +axiosInstance.defaults.baseURL = BE_BASE_URL; axiosInstance.interceptors.request.use(async config => { // If the request is sent when a token is being refreshed, delay it for 100ms. @@ -33,6 +34,13 @@ axiosInstance.interceptors.response.use(undefined, async (error: AxiosError) => // If the token is already being refreshed, resend it (will be delayed until the token has been refreshed) return axiosInstance(error.config); } else { + // If the user is on the login page, don't try to refresh their token as + // they don't have one yet + // Instead just raise the error so we can show a message + if (window.location.pathname === "/") { + throw error; + } + setRefreshTokenLock(true); try { const tokens = await refreshTokens(); @@ -55,6 +63,14 @@ axiosInstance.interceptors.response.use(undefined, async (error: AxiosError) => } setRefreshTokenLock(false); } + } else if (error.response?.status === 403) { + window.location.replace("/403-forbidden"); + } else if (error.response?.status === 404) { + // Don't go to 404 when trying to register + if (!isRegisterPath(window.location.pathname)) { + window.location.replace("/404-not-found"); + } } + throw error; }); diff --git a/frontend/src/utils/api/conflicts.ts b/frontend/src/utils/api/conflicts.ts new file mode 100644 index 000000000..03da2a990 --- /dev/null +++ b/frontend/src/utils/api/conflicts.ts @@ -0,0 +1,7 @@ +import { axiosInstance } from "./api"; +import { Conflicts } from "../../data/interfaces"; + +export async function getConflicts(edition: string): Promise { + const response = await axiosInstance.get(`/editions/${edition}/projects/conflicts`); + return response.data; +} diff --git a/frontend/src/utils/api/editions.ts b/frontend/src/utils/api/editions.ts index 907bc3dda..6b58d2527 100644 --- a/frontend/src/utils/api/editions.ts +++ b/frontend/src/utils/api/editions.ts @@ -6,23 +6,18 @@ interface EditionsResponse { editions: Edition[]; } -interface EditionFields { - name: string; - year: number; -} - /** * Get all editions the user can see. */ export async function getEditions(): Promise { - const response = await axiosInstance.get("/editions/"); + const response = await axiosInstance.get("/editions"); return response.data as EditionsResponse; } /** - * Get all edition names sorted the user can see + * Get all edition names sorted that the user can see */ -export async function getSortedEditions(): Promise { +export async function getSortedEditions(): Promise { const response = await axiosInstance.get("/users/current"); return response.data.editions; } @@ -39,9 +34,9 @@ export async function deleteEdition(name: string): Promise { * Create a new edition with the given name and year */ export async function createEdition(name: string, year: number): Promise { - const payload: EditionFields = { name: name, year: year }; + const payload = { name: name, year: year }; try { - return await axiosInstance.post("/editions/", payload); + return await axiosInstance.post("/editions", payload); } catch (error) { if (axios.isAxiosError(error) && error.response !== undefined) { return error.response; @@ -50,3 +45,11 @@ export async function createEdition(name: string, year: number): Promise { + const payload = { readonly: readonly }; + return await axiosInstance.patch(`/editions/${name}`, payload); +} diff --git a/frontend/src/utils/api/index.ts b/frontend/src/utils/api/index.ts index c8238c5c6..5ab560e1b 100644 --- a/frontend/src/utils/api/index.ts +++ b/frontend/src/utils/api/index.ts @@ -1,2 +1,3 @@ export { validateRegistrationUrl } from "./auth"; +export * as Conflicts from "./conflicts"; export * as Users from "./users"; diff --git a/frontend/src/utils/api/login.ts b/frontend/src/utils/api/login.ts index 9272775c5..700c8382b 100644 --- a/frontend/src/utils/api/login.ts +++ b/frontend/src/utils/api/login.ts @@ -10,38 +10,68 @@ interface LoginResponse { user: User; } +/** + * Set the tokens & context variables to authenticate yourself + */ +function setLogInTokens(response: LoginResponse, authCtx: AuthContextState) { + setAccessToken(response.access_token); + setRefreshToken(response.refresh_token); + ctxLogIn(response.user, authCtx); +} + /** * Function that logs the user in via their email and password. If email/password were * valid, this will automatically set the [[AuthContextState]], and set the token in LocalStorage. - * @param auth reference to the [[AuthContextState]] + * @param authCtx reference to the [[AuthContextState]] * @param email email entered * @param password password entered */ -export async function logIn( - auth: AuthContextState, +export async function logInEmail( + authCtx: AuthContextState, email: string, password: string -): Promise { +): Promise { const payload = new FormData(); payload.append("username", email); payload.append("password", password); try { - const response = await axiosInstance.post("/login/token", payload); + const response = await axiosInstance.post("/login/token/email", payload); const login = response.data as LoginResponse; - setAccessToken(login.access_token); - setRefreshToken(login.refresh_token); + setLogInTokens(login, authCtx); + return response.status; + } catch (error) { + if (axios.isAxiosError(error)) { + authCtx.setIsLoggedIn(false); + return error.response?.status || 500; + } else { + authCtx.setIsLoggedIn(null); + throw error; + } + } +} + +/** + * Function that logs the user in via GitHub OAuth. + */ +export async function logInGitHub(authCtx: AuthContextState, code: string): Promise { + const payload = new FormData(); + payload.append("code", code); + + try { + const response = await axiosInstance.post("/login/token/github", payload); + const login = response.data as LoginResponse; - ctxLogIn(login.user, auth); + setLogInTokens(login, authCtx); return true; } catch (error) { if (axios.isAxiosError(error)) { - auth.setIsLoggedIn(false); + console.log(error.response); + authCtx.setIsLoggedIn(false); return false; } else { - auth.setIsLoggedIn(null); throw error; } } diff --git a/frontend/src/utils/api/mail_overview.ts b/frontend/src/utils/api/mail_overview.ts new file mode 100644 index 000000000..111d7d47b --- /dev/null +++ b/frontend/src/utils/api/mail_overview.ts @@ -0,0 +1,65 @@ +import { Email, Student } from "../../data/interfaces"; +import { EmailType } from "../../data/enums"; +import { axiosInstance } from "./api"; +import axios from "axios"; + +/** + * A student together with its email history + */ +export interface StudentEmail { + student: Student; + emails: Email[]; +} + +/** + * Multiple studentEmails in a list + */ +export interface StudentEmails { + studentEmails: StudentEmail[]; +} + +/** + * Get the sent emails of all students + */ +export async function getMailOverview( + edition: string | undefined, + page: number, + name: string, + filters: EmailType[], + controller: AbortController +): Promise { + try { + const FormatFilters: string[] = filters.map(filter => { + return `&email_status=${Object.values(EmailType).indexOf(filter)}`; + }); + const concatted: string = FormatFilters.join(""); + + const response = await axiosInstance.get( + `/editions/${edition}/students/emails?page=${page}&name=${name}${concatted}`, + { signal: controller.signal } + ); + return response.data as StudentEmails; + } catch (error) { + if (axios.isAxiosError(error) && error.code === "ERR_CANCELED") { + return null; + } else { + throw error; + } + } +} + +/** + * Updates the Email state of the currently selected students in the table to the selected state + * from the dropdown menu + */ +export async function setStateRequest( + eventKey: string, + edition: string | undefined, + selectedStudents: number[] +) { + // post request with selected data + await axiosInstance.post(`/editions/${edition}/students/emails`, { + students_id: selectedStudents, + email_status: eventKey, + }); +} diff --git a/frontend/src/utils/api/projectRoles.ts b/frontend/src/utils/api/projectRoles.ts new file mode 100644 index 000000000..786307933 --- /dev/null +++ b/frontend/src/utils/api/projectRoles.ts @@ -0,0 +1,136 @@ +import axios from "axios"; +import { axiosInstance } from "./api"; +import { CreateProjectRole, ProjectRole } from "../../data/interfaces/projects"; + +/** + * API call to get all project roles of a project. + * @param edition The edition name. + * @param projectId The projectId where to add the new project role. + * @returns A list of the project roles. + */ +export async function getProjectRoles( + edition: string, + projectId: string +): Promise { + try { + const response = await axiosInstance.get( + "editions/" + edition + "/projects/" + projectId + "/roles" + ); + const projectRole = response.data as ProjectRole[]; + + return projectRole; + } catch (error) { + if (axios.isAxiosError(error)) { + return null; + } else { + throw error; + } + } +} + +/** + * API call to create a ProjectRole. + * @param edition The edition name. + * @param projectId The projectId where to add the new project role. + * @param skillId The id of the skill. + * @param description Optional description for this skill. + * @param slots The number of places for this skill. + * @returns The newly created project role object. + */ +export async function createProjectRole( + edition: string, + projectId: string, + skillId: number, + description: string | undefined, + slots: number +): Promise { + const payload: CreateProjectRole = { + skill_id: skillId, + description: description || "", + slots: slots, + }; + + try { + const response = await axiosInstance.post( + "editions/" + edition + "/projects/" + projectId + "/roles", + payload + ); + const projectRole = response.data as ProjectRole; + + return projectRole; + } catch (error) { + if (axios.isAxiosError(error)) { + return null; + } else { + throw error; + } + } +} + +/** + * API call to edit a ProjectRole. + * @param edition The edition name. + * @param projectRoleId The id of the project role. + * @param projectId The projectId where to change the project role. + * @param skillId The id of the skill. + * @param description Optional new description for this skill. + * @param slots The new number of places for this skill. + * @returns The updated project role object. + */ +export async function editProjectRole( + edition: string, + projectRoleId: string, + projectId: string, + skillId: number, + description: string | undefined, + slots: number +): Promise { + const payload: CreateProjectRole = { + skill_id: skillId, + description: description || "", + slots: slots, + }; + + try { + const response = await axiosInstance.patch( + "editions/" + edition + "/projects/" + projectId + "/roles/" + projectRoleId, + payload + ); + const projectRole = response.data as ProjectRole; + + return projectRole; + } catch (error) { + if (axios.isAxiosError(error)) { + return null; + } else { + throw error; + } + } +} + +/** + * API call to delete a project role + * @param edition The edition name. + * @param projectId The projectId where to delete a project role. + * @param projectRoleId The Id of the project role to delete + * @returns whether the delete was successful or not. + */ +export async function deleteProjectRole( + edition: string, + projectId: string, + projectRoleId: string +): Promise { + try { + await axiosInstance.delete( + "editions/" + edition + "/projects/" + projectId + "/roles/" + projectRoleId + ); + + return true; + } catch (error) { + if (axios.isAxiosError(error)) { + return false; + } else { + throw error; + } + } +} diff --git a/frontend/src/utils/api/projectStudents.ts b/frontend/src/utils/api/projectStudents.ts new file mode 100644 index 000000000..2df51baaf --- /dev/null +++ b/frontend/src/utils/api/projectStudents.ts @@ -0,0 +1,123 @@ +import axios from "axios"; +import { axiosInstance } from "./api"; +import { AddRoleSuggestion, ProjectRoleSuggestion } from "../../data/interfaces/projects"; + +/** + * API call to make a student role suggestion. + * @param edition The edition name. + * @param projectId The projectId where to add the suggestion. + * @param projectRoleId The id of the project role. + * @param studentId The id of the student to add. + * @param argumentation Why this student is a good fit. + * @returns if the suggestion was created successfully. + */ +export async function addStudentToProject( + edition: string, + projectId: string, + projectRoleId: string, + studentId: string, + argumentation: string | undefined +): Promise { + const payload: AddRoleSuggestion = { + argumentation: argumentation || "", + }; + + try { + const response = await axiosInstance.post( + "editions/" + + edition + + "/projects/" + + projectId + + "/roles/" + + projectRoleId + + "/students/" + + studentId, + payload + ); + return response.data as ProjectRoleSuggestion; + } catch (error) { + if (axios.isAxiosError(error)) { + return null; + } else { + throw error; + } + } +} + +/** + * API call to change a student role suggestion. + * @param edition The edition name. + * @param projectId The projectId where to add the suggestion. + * @param projectRoleId The id of the project role. + * @param studentId The id of the student to add. + * @param argumentation The updated argumentation why this student is a good fit. + * @returns if the suggestion was updated successfully. + */ +export async function patchStudentProjectRole( + edition: string, + projectId: string, + projectRoleId: string, + studentId: string, + argumentation: string | undefined +): Promise { + const payload: AddRoleSuggestion = { + argumentation: argumentation || "", + }; + + try { + const response = await axiosInstance.patch( + "editions/" + + edition + + "/projects/" + + projectId + + "/roles/" + + projectRoleId + + "/students/" + + studentId, + payload + ); + if (response) return true; + else return false; + } catch (error) { + if (axios.isAxiosError(error)) { + return false; + } else { + throw error; + } + } +} + +/** + * API call to delete a student from a project role. + * @param edition The edition name. + * @param projectId The ID of the project. + * @param projectRoleId The id of the project role. + * @param studentId The id of the student to remove from the project role. + * @returns true if the deletion was successful or false if it failed. + */ +export async function deleteStudentFromProject( + edition: string, + projectId: string, + projectRoleId: string, + studentId: string +): Promise { + try { + await axiosInstance.delete( + "editions/" + + edition + + "/projects/" + + projectId + + "/roles/" + + projectRoleId + + "/students/" + + studentId + ); + return true; + } catch (error) { + if (axios.isAxiosError(error)) { + return false; + } else { + throw error; + } + } +} diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index b3efdde13..d60b7f3f8 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -1,5 +1,10 @@ import axios from "axios"; -import { Projects, Project, CreateProject } from "../../data/interfaces/projects"; +import { + Projects, + Project, + CreateProject, + CreateProject as PatchProject, +} from "../../data/interfaces/projects"; import { axiosInstance } from "./api"; /** @@ -8,29 +13,30 @@ import { axiosInstance } from "./api"; * @param name To filter on project name. * @param ownProjects To filter on your own projects. * @param page The requested page. - * @returns + * @param controller An optional AbortController to cancel the request */ export async function getProjects( edition: string, name: string, ownProjects: boolean, - page: number + page: number, + controller: AbortController ): Promise { try { const response = await axiosInstance.get( "/editions/" + edition + - "/projects/?name=" + + "/projects?name=" + name + "&coach=" + ownProjects.toString() + "&page=" + - page.toString() + page.toString(), + { signal: controller.signal } ); - const projects = response.data as Projects; - return projects; + return response.data as Projects; } catch (error) { - if (axios.isAxiosError(error)) { + if (axios.isAxiosError(error) && error.code === "ERR_CANCELED") { return null; } else { throw error; @@ -47,8 +53,7 @@ export async function getProjects( export async function getProject(edition: string, projectId: number): Promise { try { const response = await axiosInstance.get("/editions/" + edition + "/projects/" + projectId); - const project = response.data as Project; - return project; + return response.data as Project; } catch (error) { if (axios.isAxiosError(error)) { return null; @@ -62,8 +67,7 @@ export async function getProject(edition: string, projectId: number): Promise { const payload: CreateProject = { name: name, - number_of_students: numberOfStudents, - skills: skills, + info_url: infoUrl, partners: partners, coaches: coaches, }; try { - const response = await axiosInstance.post("editions/" + edition + "/projects/", payload); + const response = await axiosInstance.post("editions/" + edition + "/projects", payload); const project = response.data as Project; return project; @@ -98,6 +100,43 @@ export async function createProject( } } +/** + * API call to edit a project. + * @param edition The edition name. + * @param projectId: The id of the project. + * @param name The name of the new project. + * @param infoUrl A info link about the project. + * @param partners The partners of the project. + * @param coaches The coaches that will coach the project. + * @returns whether or not the patch was successful. + */ +export async function patchProject( + edition: string, + projectId: number, + name: string, + infoUrl: string | null, + partners: string[], + coaches: number[] +): Promise { + const payload: PatchProject = { + name: name, + partners: partners, + coaches: coaches, + info_url: infoUrl, + }; + + try { + await axiosInstance.patch("editions/" + edition + "/projects/" + projectId, payload); + return true; + } catch (error) { + if (axios.isAxiosError(error)) { + return false; + } else { + throw error; + } + } +} + /** * API call to delete a project. * @param edition The edition name. diff --git a/frontend/src/utils/api/questions.ts b/frontend/src/utils/api/questions.ts new file mode 100644 index 000000000..5e6e11e8d --- /dev/null +++ b/frontend/src/utils/api/questions.ts @@ -0,0 +1,23 @@ +import axios from "axios"; +import { axiosInstance } from "./api"; +import { Questions } from "../../data/interfaces/questions"; + +/** + * API call to fetch all questions and answers of a student. + * @param edition The edition name. + * @param studentId The ID of the student which answers need to be fetched. + */ +export async function getQuestions(edition: string, studentId: number) { + try { + const response = await axiosInstance.get( + "/editions/" + edition + "/students/" + studentId.toString() + "/answers" + ); + return response.data as Questions; + } catch (error) { + if (axios.isAxiosError(error)) { + throw error; + } else { + throw error; + } + } +} diff --git a/frontend/src/utils/api/register.ts b/frontend/src/utils/api/register.ts index 372df866d..6487d2919 100644 --- a/frontend/src/utils/api/register.ts +++ b/frontend/src/utils/api/register.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import axios, { AxiosResponse } from "axios"; import { axiosInstance } from "./api"; interface RegisterFields { @@ -35,3 +35,19 @@ export async function register( } } } + +/** + * Function to register a user using GitHub OAuth + */ +export async function registerGithub( + edition: string, + uuid: string, + code: string +): Promise { + const payload = { + uuid: uuid, + code: code, + }; + + return await axiosInstance.post(`/editions/${edition}/register/github`, payload); +} diff --git a/frontend/src/utils/api/skills.ts b/frontend/src/utils/api/skills.ts new file mode 100644 index 000000000..df5cc2c3e --- /dev/null +++ b/frontend/src/utils/api/skills.ts @@ -0,0 +1,62 @@ +import axios from "axios"; +import { axiosInstance } from "./api"; +import { CreateSkill, Skill, Skills } from "../../data/interfaces/skills"; + +/** + * API call to get skills + * @returns a list of skills + */ +export async function getSkills(): Promise { + try { + const response = await axiosInstance.get("skills"); + const skills = response.data as Skills; + return skills; + } catch (error) { + if (axios.isAxiosError(error)) { + return null; + } else { + throw error; + } + } +} + +/** + * API call to create a Skill. + * @param name The skill name. + * @returns The newly created project role object. + */ +export async function createSkill(name: string): Promise { + const payload: CreateSkill = { + name: name, + }; + + try { + const response = await axiosInstance.post("skills", payload); + const skill = response.data as Skill; + return skill; + } catch (error) { + if (axios.isAxiosError(error)) { + return null; + } else { + throw error; + } + } +} + +/** + * API call to delete a Skill. + * @param skillId The skill id. + * @returns True if the delete was successful, false if it failed. + */ +export async function deleteSkill(skillId: string): Promise { + try { + await axiosInstance.delete("skills/" + skillId); + return true; + } catch (error) { + if (axios.isAxiosError(error)) { + return false; + } else { + throw error; + } + } +} diff --git a/frontend/src/utils/api/student_email_history.ts b/frontend/src/utils/api/student_email_history.ts new file mode 100644 index 000000000..64c9fa1a0 --- /dev/null +++ b/frontend/src/utils/api/student_email_history.ts @@ -0,0 +1,12 @@ +import { EmailHistoryList } from "../../data/interfaces"; +import { axiosInstance } from "./api"; +/** + * Get the full email history for a student + */ +export async function getEmails( + editionId: string | undefined, + studentId: string | undefined +): Promise { + const response = await axiosInstance.get(`/editions/${editionId}/students/${studentId}/emails`); + return response.data as EmailHistoryList; +} diff --git a/frontend/src/utils/api/students.ts b/frontend/src/utils/api/students.ts new file mode 100644 index 000000000..c7daae59b --- /dev/null +++ b/frontend/src/utils/api/students.ts @@ -0,0 +1,117 @@ +import axios from "axios"; +import { Student, Students } from "../../data/interfaces/students"; +import { axiosInstance } from "./api"; +import { DropdownRole } from "../../components/StudentsComponents/StudentListFilters/RolesFilter/RolesFilter"; + +/** + * API call to get students (and filter them). + * @param edition The edition name. + * @param nameFilter name to filter on. + * @param rolesFilter roles to filter on. + * @param alumniFilter check to filter on. + * @param studentCoachVolunteerFilter check to filter on. + * @param suggestedFilter check to filter on. + * @param confirmFilter confirmation state to filter on. + * @param page The page to fetch. + * @param controller An optional AbortController to cancel the request + */ +export async function getStudents( + edition: string, + nameFilter: string, + rolesFilter: DropdownRole[], + alumniFilter: boolean, + studentCoachVolunteerFilter: boolean, + suggestedFilter: boolean, + confirmFilter: DropdownRole[], + page: number, + controller: AbortController +): Promise { + let rolesRequestField: string = ""; + + for (const role of rolesFilter) { + rolesRequestField += "skill_ids=" + role.value.toString() + "&"; + } + let confirmRequestField: string = ""; + for (const confirmField of confirmFilter) { + confirmRequestField += "decisions=" + confirmField.value.toString() + "&"; + } + try { + const response = await axiosInstance.get( + "/editions/" + + edition + + "/students?name=" + + nameFilter + + "&alumni=" + + alumniFilter + + "&own_suggestions=" + + suggestedFilter + + "&" + + rolesRequestField + + "&student_coach=" + + studentCoachVolunteerFilter + + "&" + + confirmRequestField + + "&page=" + + page, + { signal: controller.signal } + ); + return response.data as Students; + } catch (error) { + if (axios.isAxiosError(error) && error.code === "ERR_CANCELED") { + return null; + } else { + throw error; + } + } +} + +/** + * API call to get a specific student. + * @param edition The edition name. + * @param studentId The ID of the student. + */ +export async function getStudent(edition: string, studentId: number): Promise { + const request = "/editions/" + edition + "/students/" + studentId.toString(); + const response = await axiosInstance.get(request); + return response.data.student as Student; +} + +/** + * API call to delete a student. + * @param edition The edition name. + * @param studentId The ID of the student that needs to be deleted. + */ +export async function removeStudent(edition: string, studentId: number): Promise { + try { + const request = "/editions/" + edition + "/students/" + studentId.toString(); + await axiosInstance.delete(request); + return 201; + } catch (error) { + if (axios.isAxiosError(error)) { + return 422; + } else { + throw error; + } + } +} + +/** + * API call to make a suggestion on a student. + * @param edition The edition name. + * @param studentId The ID of the student on who a suggestion needs to be made. + * @param suggestionArg The Suggestion value. + * @param argumentationArg The argumentation for this suggestion. + */ +export async function makeSuggestion( + edition: string, + studentId: string, + suggestionArg: number, + argumentationArg: string +): Promise { + const request = "/editions/" + edition + "/students/" + studentId.toString() + "/suggestions"; + await axiosInstance.post(request, { + suggestion: suggestionArg, + argumentation: argumentationArg, + }); + return 201; +} diff --git a/frontend/src/utils/api/suggestions.ts b/frontend/src/utils/api/suggestions.ts new file mode 100644 index 000000000..bf4f1539d --- /dev/null +++ b/frontend/src/utils/api/suggestions.ts @@ -0,0 +1,51 @@ +import axios from "axios"; +import { axiosInstance } from "./api"; +import { Suggestion, Suggestions } from "../../data/interfaces/suggestions"; + +/** + * API call to fetch all suggestion on a student. + * @param edition The edition name. + * @param studentId The ID of the student which suggestions need to be fetched. + */ +export async function getSuggestions(edition: string, studentId: number) { + try { + const response = await axiosInstance.get( + "/editions/" + edition + "/students/" + studentId.toString() + "/suggestions" + ); + return response.data as Suggestions; + } catch (error) { + if (axios.isAxiosError(error)) { + throw error; + } else { + throw error; + } + } +} + +/** + * API call for admins to make a definitive decision on a student. + * @param edition The edition name. + * @param studentId The ID of the student to make a decision on. + * @param confirmValue The decision to give this student. + */ +export async function confirmStudent(edition: string, studentId: string, confirmValue: number) { + const response = await axiosInstance.put( + "/editions/" + edition + "/students/" + studentId.toString() + "/decision", + { decision: confirmValue } + ); + return response.status === 204; +} + +/** + * API call to fetch a suggestion by its id + */ +export async function getSuggestionById( + edition: string, + studentId: string, + suggestionId: string +): Promise { + const response = await axiosInstance.get( + `/editions/${edition}/students/${studentId}/suggestions/${suggestionId}` + ); + return response.data as Suggestion; +} diff --git a/frontend/src/utils/api/users/admins.ts b/frontend/src/utils/api/users/admins.ts index cc6b8bf12..5bf0dbdb6 100644 --- a/frontend/src/utils/api/users/admins.ts +++ b/frontend/src/utils/api/users/admins.ts @@ -19,9 +19,8 @@ export async function getAdmins(page: number, name: string): Promise * Make the given user admin. * @param {number} userId The id of the user. */ -export async function addAdmin(userId: number): Promise { - const response = await axiosInstance.patch(`/users/${userId}`, { admin: true }); - return response.status === 204; +export async function addAdmin(userId: number) { + await axiosInstance.patch(`/users/${userId}`, { admin: true }); } /** @@ -29,8 +28,7 @@ export async function addAdmin(userId: number): Promise { * @param {number} userId The id of the user. */ export async function removeAdmin(userId: number) { - const response = await axiosInstance.patch(`/users/${userId}`, { admin: false }); - return response.status === 204; + await axiosInstance.patch(`/users/${userId}`, { admin: false }); } /** @@ -38,7 +36,6 @@ export async function removeAdmin(userId: number) { * @param {number} userId The id of the user. */ export async function removeAdminAndCoach(userId: number) { - const response2 = await axiosInstance.delete(`/users/${userId}/editions`); - const response1 = await axiosInstance.patch(`/users/${userId}`, { admin: false }); - return response1.status === 204 && response2.status === 204; + await axiosInstance.delete(`/users/${userId}/editions`); + await axiosInstance.patch(`/users/${userId}`, { admin: false }); } diff --git a/frontend/src/utils/api/users/coaches.ts b/frontend/src/utils/api/users/coaches.ts index 9762564fb..155f3b7d5 100644 --- a/frontend/src/utils/api/users/coaches.ts +++ b/frontend/src/utils/api/users/coaches.ts @@ -1,21 +1,40 @@ import { UsersList } from "./users"; import { axiosInstance } from "../api"; +import axios from "axios"; /** * Get a page from all coaches from the given edition. * @param edition The edition name. * @param name The username to filter. * @param page The requested page. + * @param controller An optional AbortController to cancel the request */ -export async function getCoaches(edition: string, name: string, page: number): Promise { - if (name) { +export async function getCoaches( + edition: string, + name: string, + page: number, + controller: AbortController | null = null +): Promise { + if (controller === null) { const response = await axiosInstance.get( - `/users/?edition=${edition}&page=${page}&name=${name}` + `/users?edition=${edition}&page=${page}&name=${name}` ); return response.data as UsersList; + } else { + try { + const response = await axiosInstance.get( + `/users?edition=${edition}&page=${page}&name=${name}`, + { signal: controller.signal } + ); + return response.data as UsersList; + } catch (error) { + if (axios.isAxiosError(error) && error.code === "ERR_CANCELED") { + return null; + } else { + throw error; + } + } } - const response = await axiosInstance.get(`/users/?edition=${edition}&page=${page}`); - return response.data as UsersList; } /** @@ -23,18 +42,16 @@ export async function getCoaches(edition: string, name: string, page: number): P * @param {number} userId The user's id. * @param {string} edition The edition's name. */ -export async function removeCoachFromEdition(userId: number, edition: string): Promise { - const response = await axiosInstance.delete(`/users/${userId}/editions/${edition}`); - return response.status === 204; +export async function removeCoachFromEdition(userId: number, edition: string) { + await axiosInstance.delete(`/users/${userId}/editions/${edition}`); } /** * Remove a user as coach from all editions. * @param {number} userId The user's id. */ -export async function removeCoachFromAllEditions(userId: number): Promise { - const response = await axiosInstance.delete(`/users/${userId}/editions`); - return response.status === 204; +export async function removeCoachFromAllEditions(userId: number) { + await axiosInstance.delete(`/users/${userId}/editions`); } /** @@ -42,7 +59,6 @@ export async function removeCoachFromAllEditions(userId: number): Promise { - const response = await axiosInstance.post(`/users/${userId}/editions/${edition}`); - return response.status === 204; +export async function addCoachToEdition(userId: number, edition: string) { + await axiosInstance.post(`/users/${userId}/editions/${edition}`); } diff --git a/frontend/src/utils/api/users/requests.ts b/frontend/src/utils/api/users/requests.ts index b40888652..9950caa6d 100644 --- a/frontend/src/utils/api/users/requests.ts +++ b/frontend/src/utils/api/users/requests.ts @@ -1,5 +1,6 @@ import { User } from "./users"; import { axiosInstance } from "../api"; +import axios from "axios"; /** * Interface of a request @@ -21,36 +22,41 @@ export interface GetRequestsResponse { * @param edition The edition's name. * @param name String which every request's user's name needs to contain * @param page The pagenumber to fetch. + * @param controller An optional AbortController to cancel the request */ export async function getRequests( edition: string, name: string, - page: number -): Promise { - if (name) { + page: number, + controller: AbortController +): Promise { + try { const response = await axiosInstance.get( - `/users/requests?edition=${edition}&page=${page}&user=${name}` + `/users/requests?edition=${edition}&page=${page}&user=${name}`, + { signal: controller.signal } ); return response.data as GetRequestsResponse; + } catch (error) { + if (axios.isAxiosError(error) && error.code === "ERR_CANCELED") { + return null; + } else { + throw error; + } } - const response = await axiosInstance.get(`/users/requests?edition=${edition}&page=${page}`); - return response.data as GetRequestsResponse; } /** * Accept a coach request. * @param {number} requestId The id of the request. */ -export async function acceptRequest(requestId: number): Promise { - const response = await axiosInstance.post(`/users/requests/${requestId}/accept`); - return response.status === 204; +export async function acceptRequest(requestId: number) { + await axiosInstance.post(`/users/requests/${requestId}/accept`); } /** * Reject a coach request. * @param {number} requestId The id of the request.s */ -export async function rejectRequest(requestId: number): Promise { - const response = await axiosInstance.post(`/users/requests/${requestId}/reject`); - return response.status === 204; +export async function rejectRequest(requestId: number) { + await axiosInstance.post(`/users/requests/${requestId}/reject`); } diff --git a/frontend/src/utils/api/users/users.ts b/frontend/src/utils/api/users/users.ts index bf790f2c6..cf57453f4 100644 --- a/frontend/src/utils/api/users/users.ts +++ b/frontend/src/utils/api/users/users.ts @@ -52,12 +52,11 @@ export async function getUsersExcludeEdition( ): Promise { if (name) { const response = await axiosInstance.get( - `/users/?page=${page}&exclude_edition=${edition}&name=${name}` + `/users?page=${page}&exclude_edition=${edition}&name=${name}` ); - console.log(response.data); return response.data as UsersList; } - const response = await axiosInstance.get(`/users/?exclude_edition=${edition}&page=${page}`); + const response = await axiosInstance.get(`/users?exclude_edition=${edition}&page=${page}`); return response.data as UsersList; } @@ -68,9 +67,9 @@ export async function getUsersExcludeEdition( */ export async function getUsersNonAdmin(name: string, page: number): Promise { if (name) { - const response = await axiosInstance.get(`/users/?page=${page}&admin=false&name=${name}`); + const response = await axiosInstance.get(`/users?page=${page}&admin=false&name=${name}`); return response.data as UsersList; } - const response = await axiosInstance.get(`/users/?admin=false&page=${page}`); + const response = await axiosInstance.get(`/users?admin=false&page=${page}`); return response.data as UsersList; } diff --git a/frontend/src/utils/logic/editions.ts b/frontend/src/utils/logic/editions.ts new file mode 100644 index 000000000..bc3c17c5f --- /dev/null +++ b/frontend/src/utils/logic/editions.ts @@ -0,0 +1,9 @@ +import { Edition } from "../../data/interfaces"; + +/** + * Check if an edition is read-only + */ +export function isReadonlyEdition(name: string | undefined, editions: Edition[]): boolean { + if (!name) return false; + return editions.find(e => e.name === name)?.readonly || false; +} diff --git a/frontend/src/utils/logic/index.ts b/frontend/src/utils/logic/index.ts index dfb0a10c0..881018449 100644 --- a/frontend/src/utils/logic/index.ts +++ b/frontend/src/utils/logic/index.ts @@ -1 +1,3 @@ -export { getBestRedirect } from "./routes"; +export { isReadonlyEdition } from "./editions"; +export { createRedirectUri, decodeRegistrationLink } from "./registration"; +export { getBestRedirect, isRegisterPath } from "./routes"; diff --git a/frontend/src/utils/logic/project.ts b/frontend/src/utils/logic/project.ts new file mode 100644 index 000000000..fc017b4e3 --- /dev/null +++ b/frontend/src/utils/logic/project.ts @@ -0,0 +1,21 @@ +import { Project, CreateProject as EditProject } from "../../data/interfaces"; + +export default function projectToEditProject(project: Project): EditProject { + const coachesIds: number[] = []; + project.coaches.forEach(coach => { + coachesIds.push(coach.userId); + }); + + const partners: string[] = []; + project.partners.forEach(partner => { + partners.push(partner.name); + }); + + const editProject: EditProject = { + name: project.name, + info_url: project.infoUrl, + partners: partners, + coaches: coachesIds, + }; + return editProject; +} diff --git a/frontend/src/utils/logic/registration.ts b/frontend/src/utils/logic/registration.ts index d6d03a42c..dbca44b2a 100644 --- a/frontend/src/utils/logic/registration.ts +++ b/frontend/src/utils/logic/registration.ts @@ -1,3 +1,6 @@ +import { OAuthProvider } from "../../data/enums"; +import { FE_BASE_URL } from "../../settings"; + const Buffer = require("buffer/").Buffer; /** @@ -28,3 +31,14 @@ export function decodeRegistrationLink( return null; } } + +/** + * Compose a redirect uri for oauth providers + */ +export function createRedirectUri( + provider: OAuthProvider, + data: { edition: string; uuid: string } +): string { + const encodedEdition = encodeURIComponent(data.edition); + return `${FE_BASE_URL}/register/redirect?provider=${provider}&edition=${encodedEdition}&uuid=${data.uuid}`; +} diff --git a/frontend/src/utils/logic/routes.test.ts b/frontend/src/utils/logic/routes.test.ts index 0f370b567..457ad3c7c 100644 --- a/frontend/src/utils/logic/routes.test.ts +++ b/frontend/src/utils/logic/routes.test.ts @@ -1,10 +1,19 @@ -import { getBestRedirect } from "./routes"; +import { getBestRedirect, isRegisterPath } from "./routes"; /** * Note: all tests here also test the one with a trailing slash (/) because I'm paranoid * about the asterisk matching it */ +test("/students/states stays there", () => { + expect(getBestRedirect("/editions/old/students/states", "new")).toEqual( + "/editions/new/students/states" + ); + expect(getBestRedirect("/editions/old/students/states/", "new")).toEqual( + "/editions/new/students/states" + ); +}); + test("/students stays there", () => { expect(getBestRedirect("/editions/old/students", "new")).toEqual("/editions/new/students"); expect(getBestRedirect("/editions/old/students/", "new")).toEqual("/editions/new/students"); @@ -37,3 +46,15 @@ test("/editions stays there", () => { expect(getBestRedirect("/editions", "new")).toEqual("/editions"); expect(getBestRedirect("/editions/", "new")).toEqual("/editions"); }); + +test("register link detected correctly", () => { + expect(isRegisterPath("/register/this-is-an-id")).toBeTruthy(); +}); + +test("other links are not detected as register paths", () => { + expect(isRegisterPath("/")).toBeFalsy(); + expect(isRegisterPath("/register")).toBeFalsy(); + expect(isRegisterPath("/editions/:uid")).toBeFalsy(); + expect(isRegisterPath("/editions/register")).toBeFalsy(); + expect(isRegisterPath("/projects/register/uid")).toBeFalsy(); +}); diff --git a/frontend/src/utils/logic/routes.ts b/frontend/src/utils/logic/routes.ts index 68260dc15..5d1352787 100644 --- a/frontend/src/utils/logic/routes.ts +++ b/frontend/src/utils/logic/routes.ts @@ -5,7 +5,12 @@ import { matchPath } from "react-router-dom"; * Boils down to the most-specific route that can be used across editions */ export function getBestRedirect(location: string, editionName: string): string { - // All /student/X routes should go to /students + // Students/states should stay at /students/states + if (matchPath({ path: "/editions/:edition/students/states" }, location)) { + return `/editions/${editionName}/students/states`; + } + + // All remaining /student/X routes should go to /students if (matchPath({ path: "/editions/:edition/students/*" }, location)) { return `/editions/${editionName}/students`; } @@ -33,3 +38,10 @@ export function getBestRedirect(location: string, editionName: string): string { // All the rest: go to /editions return "/editions"; } + +/** + * Check if the current location is the register page + */ +export function isRegisterPath(location: string): boolean { + return Boolean(matchPath({ path: "/register/:id" }, location)); +} diff --git a/frontend/src/utils/session-storage/auth.ts b/frontend/src/utils/session-storage/auth.ts new file mode 100644 index 000000000..ee14556a3 --- /dev/null +++ b/frontend/src/utils/session-storage/auth.ts @@ -0,0 +1,19 @@ +import { SessionStorageKey } from "../../data/enums"; + +/** + * Return the random state stored from SessionStorage used in the registration process + */ +export function getRegisterState(): string | null { + return sessionStorage.getItem(SessionStorageKey.REGISTER_STATE); +} + +/** + * Set the state in SessionStorage to something random to prevent CSRF-attacks + */ +export function generateRegisterState(): string { + // Generate a random string + const state = Math.random().toString(36).slice(2); + sessionStorage.setItem(SessionStorageKey.REGISTER_STATE, state); + + return state; +} diff --git a/frontend/src/utils/session-storage/index.ts b/frontend/src/utils/session-storage/index.ts index 2b2ae5a58..b3376e017 100644 --- a/frontend/src/utils/session-storage/index.ts +++ b/frontend/src/utils/session-storage/index.ts @@ -1 +1,2 @@ +export { getRegisterState, generateRegisterState } from "./auth"; export { getCurrentEdition, setCurrentEdition } from "./current-edition"; diff --git a/frontend/src/utils/session-storage/student-filters.ts b/frontend/src/utils/session-storage/student-filters.ts new file mode 100644 index 000000000..7d298c588 --- /dev/null +++ b/frontend/src/utils/session-storage/student-filters.ts @@ -0,0 +1,85 @@ +import { SessionStorageKey } from "../../data/enums"; +import { DropdownRole } from "../../components/StudentsComponents/StudentListFilters/RolesFilter/RolesFilter"; + +export function getNameFilter(): string { + const nameFilter = sessionStorage.getItem(SessionStorageKey.NAME_FILTER); + return nameFilter === null ? "" : nameFilter; +} + +export function getAlumniFilter(): boolean { + const alumniFilter = sessionStorage.getItem(SessionStorageKey.ALUMNI_FILTER); + return alumniFilter === null ? false : JSON.parse(alumniFilter); +} + +export function getStudentCoachVolunteerFilter(): boolean { + const studentCoachVolunteerFilter = sessionStorage.getItem( + SessionStorageKey.STUDENT_COACH_VOLUNTEER_FILTER + ); + return studentCoachVolunteerFilter === null ? false : JSON.parse(studentCoachVolunteerFilter); +} + +export function getSuggestedFilter(): boolean { + const suggestedFilter = sessionStorage.getItem(SessionStorageKey.SUGGESTED_FILTER); + return suggestedFilter === null ? false : JSON.parse(suggestedFilter); +} + +export function getRolesFilter(): DropdownRole[] { + const rolesFilter = sessionStorage.getItem(SessionStorageKey.ROLES_FILTER); + return rolesFilter === null ? [] : JSON.parse(rolesFilter); +} + +export function getConfirmFilter(): DropdownRole[] { + const confirmFilter = sessionStorage.getItem(SessionStorageKey.CONFIRM_FILTER); + return confirmFilter === null ? [] : JSON.parse(confirmFilter); +} + +export function setNameFilterStorage(nameFilter: string | null) { + if (nameFilter === null) { + sessionStorage.removeItem(SessionStorageKey.NAME_FILTER); + } else { + sessionStorage.setItem(SessionStorageKey.NAME_FILTER, nameFilter); + } +} + +export function setAlumniFilterStorage(alumniFilter: string | null) { + if (alumniFilter === null) { + sessionStorage.removeItem(SessionStorageKey.ALUMNI_FILTER); + } else { + sessionStorage.setItem(SessionStorageKey.ALUMNI_FILTER, alumniFilter); + } +} + +export function setStudentCoachVolunteerFilterStorage(studentCoachVolunteerFilter: string | null) { + if (studentCoachVolunteerFilter === null) { + sessionStorage.removeItem(SessionStorageKey.STUDENT_COACH_VOLUNTEER_FILTER); + } else { + sessionStorage.setItem( + SessionStorageKey.STUDENT_COACH_VOLUNTEER_FILTER, + studentCoachVolunteerFilter + ); + } +} + +export function setSuggestedFilterStorage(suggestedFilter: string | null) { + if (suggestedFilter === null) { + sessionStorage.removeItem(SessionStorageKey.SUGGESTED_FILTER); + } else { + sessionStorage.setItem(SessionStorageKey.SUGGESTED_FILTER, suggestedFilter); + } +} + +export function setRolesFilterStorage(rolesFilter: string | null) { + if (rolesFilter === null) { + sessionStorage.removeItem(SessionStorageKey.ROLES_FILTER); + } else { + sessionStorage.setItem(SessionStorageKey.ROLES_FILTER, rolesFilter); + } +} + +export function setConfirmFilterStorage(confirmFilter: string | null) { + if (confirmFilter === null) { + sessionStorage.removeItem(SessionStorageKey.CONFIRM_FILTER); + } else { + sessionStorage.setItem(SessionStorageKey.CONFIRM_FILTER, confirmFilter); + } +} diff --git a/frontend/src/views/AdminsPage/AdminsPage.tsx b/frontend/src/views/AdminsPage/AdminsPage.tsx index 29ec9b2be..ee151d34d 100644 --- a/frontend/src/views/AdminsPage/AdminsPage.tsx +++ b/frontend/src/views/AdminsPage/AdminsPage.tsx @@ -1,97 +1,102 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { AdminsContainer } from "./styles"; import { getAdmins } from "../../utils/api/users/admins"; -import { Error, SpinnerContainer } from "../../components/UsersComponents/Requests/styles"; import { AddAdmin, AdminList } from "../../components/AdminsComponents"; -import { Spinner } from "react-bootstrap"; import { User } from "../../utils/api/users/users"; -import { SearchInput } from "../../components/styles"; +import { SearchBar } from "../../components/Common/Forms"; +import { SearchFieldDiv, TableDiv } from "../../components/Common/Users/styles"; +import LoadSpinner from "../../components/Common/LoadSpinner"; +import { toast } from "react-toastify"; export default function AdminsPage() { + const [allAdmins, setAllAdmins] = useState([]); const [admins, setAdmins] = useState([]); - const [gettingData, setGettingData] = useState(false); + const [loading, setLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [gotData, setGotData] = useState(false); - const [error, setError] = useState(""); - const [moreAdminsAvailable, setMoreAdminsAvailable] = useState(true); - async function getData(page: number, filter: string | undefined = undefined) { - if (filter === undefined) { - filter = searchTerm; - } - setGettingData(true); - setError(""); - try { - const response = await getAdmins(page, filter); - if (response.users.length !== 25) { - setMoreAdminsAvailable(false); - } + const getData = useCallback(async () => { + let adminsAvailable = true; + let page = 0; + let newAdmins: User[] = []; + while (adminsAvailable) { + const response = await toast.promise(getAdmins(page, searchTerm), { + error: "Failed to receive admins", + }); if (page === 0) { - setAdmins(response.users); + newAdmins = response.users; } else { - setAdmins(admins.concat(response.users)); + newAdmins = newAdmins.concat(response.users); } - - setGotData(true); - setGettingData(false); - } catch (exception) { - setError("Oops, something went wrong..."); - setGettingData(false); + adminsAvailable = response.users.length !== 0; + page += 1; } - } + setAdmins(newAdmins); + setAllAdmins(newAdmins); + + setGotData(true); + setLoading(false); + }, [searchTerm]); useEffect(() => { - if (!gotData && !gettingData && !error) { - getData(0); + if (!gotData && !loading) { + setLoading(true); + getData(); } - }); - - function filter(word: string) { - setGotData(false); - setSearchTerm(word); - setAdmins([]); - setMoreAdminsAvailable(true); - getData(0, word); - } + }, [gotData, loading, getData]); - function adminAdded(user: User) { + function addAdmin(user: User) { + setAllAdmins(allAdmins.concat([user])); if (user.name.includes(searchTerm)) { setAdmins([user].concat(admins)); } } + function removeAdmin(user: User) { + setAllAdmins(allAdmins.filter(el => el !== user)); + setAdmins(admins.filter(el => el !== user)); + } + + function filter(searchTerm: string) { + setSearchTerm(searchTerm); + const newAdmins: User[] = []; + for (const admin of allAdmins) { + if (admin.name.toUpperCase().includes(searchTerm.toUpperCase())) { + newAdmins.push(admin); + } + } + setAdmins(newAdmins); + } + let list; if (admins.length === 0) { - if (gettingData) { - list = ( - - - - ); - } else if (gotData) { - list =
    No admins found
    ; + if (loading) { + list = ; } else { - list = {error}; + list =
    No admins found
    ; } } else { list = ( getData(0)} - getMoreAdmins={getData} - moreAdminsAvailable={moreAdminsAvailable} + removeAdmin={removeAdmin} /> ); } return ( - filter(e.target.value)} /> - - {list} - {error} + + filter(e.target.value)} + value={searchTerm} + placeholder="Search name..." + /> + + + {list} ); } diff --git a/frontend/src/views/AdminsPage/styles.ts b/frontend/src/views/AdminsPage/styles.ts index be7039a25..bdf975013 100644 --- a/frontend/src/views/AdminsPage/styles.ts +++ b/frontend/src/views/AdminsPage/styles.ts @@ -1,7 +1,7 @@ import styled from "styled-components"; export const AdminsContainer = styled.div` - width: 50%; - min-width: 600px; + width: 45em; + max-width: 90%; margin: 10px auto auto; `; diff --git a/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx b/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx index bb7abeb53..844d2a2cf 100644 --- a/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx +++ b/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx @@ -1,10 +1,14 @@ -import { Button, Form, Spinner } from "react-bootstrap"; -import React, { SyntheticEvent, useState } from "react"; +import { Form, Spinner } from "react-bootstrap"; +import { SyntheticEvent, useState } from "react"; import { createEdition, getSortedEditions } from "../../utils/api/editions"; import { useNavigate } from "react-router-dom"; -import { CreateEditionDiv, Error, FormGroup, ButtonDiv } from "./styles"; +import { CreateEditionDiv, FormGroup, ButtonDiv, CancelButton } from "./styles"; import { useAuth } from "../../contexts"; import { setCurrentEdition } from "../../utils/session-storage"; +import { toast } from "react-toastify"; +import { BiArrowBack } from "react-icons/bi"; +import { CreateButton } from "../../components/Common/Buttons"; +import { FormControl } from "../../components/Common/Forms"; /** * Page to create a new edition. @@ -17,24 +21,33 @@ export default function CreateEditionPage() { const [name, setName] = useState(""); const [year, setYear] = useState(currentYear.toString()); - const [nameError, setNameError] = useState(undefined); - const [yearError, setYearError] = useState(undefined); - const [error, setError] = useState(undefined); + const [nameError, setNameError] = useState(false); + const [yearError, setYearError] = useState(false); const [loading, setLoading] = useState(false); async function sendEdition(name: string, year: number): Promise { - const response = await createEdition(name, year); + const response = await toast.promise( + createEdition(name, year), + { + pending: "Creating new edition", + error: "Connection issue", + }, + { toastId: "createEdition" } + ); if (response.status === 201) { const allEditions = await getSortedEditions(); setEditions(allEditions); setCurrentEdition(response.data.name); + toast.success("Successfully made new edition", { toastId: "createEditionSuccess" }); return true; } else if (response.status === 409) { - setNameError("Edition name already exists."); + setNameError(true); + toast.error("Edition name already exists", { toastId: "createEditionNameExists" }); } else if (response.status === 422) { - setNameError("Invalid edition name."); + setNameError(true); + toast.error("Invalid edition name", { toastId: "createEditionBadName" }); } else { - setError("Something went wrong."); + toast.error("Something went wrong", { toastId: "createEditionError" }); } return false; } @@ -44,37 +57,50 @@ export default function CreateEditionPage() { event.preventDefault(); let correct = true; - // Edition name can't contain spaces and must be at least 5 long. - if (!/^([^ ]{5,})$/.test(name)) { - if (name.includes(" ")) { - setNameError("Edition name can't contain spaces."); - } else if (name.length < 5) { - setNameError("Edition name must be longer than 4 characters."); - } else { - setNameError("Invalid edition name."); - } + const newName = name.replaceAll(" ", "_"); + if (newName !== name) { + toast.info("Edition name can't contain spaces"); + setName(newName); + + correct = false; + } + if (!/^[A-Za-z0-9\-_]+$/.test(newName)) { + setNameError(true); + toast.error("Invalid edition name. Allowed characters: a-Z, 0-9, - and _", { + toastId: "createEditionBadName", + }); + correct = false; } const yearNumber = Number(year); if (isNaN(yearNumber)) { correct = false; - setYearError("Invalid year."); + setYearError(true); + toast.error("Invalid year", { toastId: "createEditionYearNoNumber" }); } else { if (yearNumber < currentYear) { correct = false; - setYearError("New editions can't be in the past."); + setYearError(true); + toast.error("New editions can't be in the past", { + toastId: "createEditionPastYear", + }); } else if (yearNumber > 3000) { correct = false; - setYearError("Invalid year."); + setYearError(true); + toast.error("Invalid year", { toastId: "createEditionYearName" }); } } let success = false; if (correct) { setLoading(true); - success = await sendEdition(name, yearNumber); - setLoading(false); + try { + success = await sendEdition(name, yearNumber); + setLoading(false); + } catch (error) { + setLoading(false); + } } if (success) { @@ -87,11 +113,7 @@ export default function CreateEditionPage() { if (loading) { submitButton = ; } else { - submitButton = ( - - ); + submitButton = ; } return ( @@ -99,39 +121,38 @@ export default function CreateEditionPage() {
    Edition name - { setName(e.target.value); - setNameError(undefined); - setError(undefined); + setNameError(false); }} /> - {nameError} Edition year - { setYear(e.target.value); - setYearError(undefined); - setError(undefined); + setYearError(false); }} /> - {yearError} - {submitButton} - {error} + + navigate("/editions")}> + + Cancel + + {submitButton} +
    ); diff --git a/frontend/src/views/CreateEditionPage/styles.ts b/frontend/src/views/CreateEditionPage/styles.ts index 4cfef2415..e647f1594 100644 --- a/frontend/src/views/CreateEditionPage/styles.ts +++ b/frontend/src/views/CreateEditionPage/styles.ts @@ -1,11 +1,5 @@ import styled from "styled-components"; -import { Form } from "react-bootstrap"; - -export const Error = styled.div` - color: var(--osoc_red); - width: 100%; - margin: 20px auto auto; -`; +import { Form, Button } from "react-bootstrap"; export const CreateEditionDiv = styled.div` width: 80%; @@ -18,7 +12,24 @@ export const FormGroup = styled(Form.Group)` `; export const ButtonDiv = styled.div` - margin-top: 20px; - margin-bottom: 20px; - float: right; + margin: 15px auto; + display: flex; + width: fit-content; + align-items: center; + vertical-align: middle; +`; + +export const CancelButton = styled(Button)` + display: flex; + align-items: center; + margin-right: 5px; + background-color: #131329; + color: white; + border-color: #131329; + + &:hover { + background-color: #131325; + color: white; + border-color: #131325; + } `; diff --git a/frontend/src/views/EditionsPage/EditionsPage.tsx b/frontend/src/views/EditionsPage/EditionsPage.tsx index f2f7dc0a4..0072e7eac 100644 --- a/frontend/src/views/EditionsPage/EditionsPage.tsx +++ b/frontend/src/views/EditionsPage/EditionsPage.tsx @@ -1,6 +1,6 @@ import { EditionsTable, NewEditionButton } from "../../components/EditionsPage"; -import { EditionsPageContainer } from "./styles"; import { useNavigate } from "react-router-dom"; +import { PageContainer } from "../../App.styles"; /** * Page where users can see all editions they can access, @@ -10,10 +10,9 @@ export default function EditionsPage() { const navigate = useNavigate(); return ( - -

    Editions

    + navigate("/editions/new")} /> -
    + ); } diff --git a/frontend/src/views/EditionsPage/styles.ts b/frontend/src/views/EditionsPage/styles.ts deleted file mode 100644 index cfafda700..000000000 --- a/frontend/src/views/EditionsPage/styles.ts +++ /dev/null @@ -1,11 +0,0 @@ -import styled from "styled-components"; -import Container from "react-bootstrap/Container"; - -export const EditionsPageContainer = styled(Container).attrs(() => ({ - className: "mt-2", -}))` - display: flex; - flex-direction: column; - justify-content: center; - margin: auto; -`; diff --git a/frontend/src/views/LoginPage/LoginPage.test.tsx b/frontend/src/views/LoginPage/LoginPage.test.tsx new file mode 100644 index 000000000..cc2b79dd1 --- /dev/null +++ b/frontend/src/views/LoginPage/LoginPage.test.tsx @@ -0,0 +1,38 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import LoginPage from "./LoginPage"; + +import "@testing-library/jest-dom"; + +const mockedUsedNavigate = jest.fn(); + +jest.mock("react-router-dom", () => ({ + ...jest.requireActual("react-router-dom"), + useNavigate: () => mockedUsedNavigate, +})); + +test("has an email field and it changes", async () => { + render(); + const emailField = screen.getByPlaceholderText("name@example.com"); + expect(emailField).toHaveValue(""); + await userEvent.type(emailField, "email@email.com"); + expect(emailField).toHaveValue("email@email.com"); +}); + +test("has an password field and it changes", async () => { + render(); + const passwordField = screen.getByPlaceholderText("Password"); + expect(passwordField).toHaveValue(""); + await userEvent.type(passwordField, "wachtwoord"); + expect(passwordField).toHaveValue("wachtwoord"); +}); + +test("has headings", () => { + render(); + const headings = screen.getAllByRole("heading"); + expect(headings[0]).toHaveTextContent("Hi there!"); + expect(headings[1]).toHaveTextContent("Welcome to the Open Summer of Code selections app."); + expect(headings[2]).toHaveTextContent( + "After you've logged in with your account, we'll enable your account so you can get started. An admin will verify you as soon as possible." + ); +}); diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index c3818c93f..6d0d0e485 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -1,26 +1,38 @@ -import { useEffect, useState } from "react"; +import { FormEvent, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { logIn } from "../../utils/api/login"; +import { logInEmail } from "../../utils/api/login"; -import { Email, Password, SocialButtons, WelcomeText } from "../../components/LoginComponents"; +import CreateButton from "../../components/Common/Buttons/CreateButton"; +import { SocialButtons, WelcomeText } from "../../components/LoginComponents"; import { EmailLoginContainer, - LoginButton, LoginContainer, LoginPageContainer, NoAccount, VerticalDivider, } from "./styles"; -import { useAuth } from "../../contexts/auth-context"; +import { useAuth } from "../../contexts"; +import { FormControl } from "../../components/Common/Forms"; +import FloatingLabel from "react-bootstrap/FloatingLabel"; +import Form from "react-bootstrap/Form"; +import { toast } from "react-toastify"; + +export enum ToastId { + EmptyEmail = "login-empty-email", + EmptyPassword = "login-empty-password", + PendingRequest = "login-pending-request", +} /** * Page where users can log in to the application. */ export default function LoginPage() { const [email, setEmail] = useState(""); + const [emailValid, setEmailValid] = useState(true); const [password, setPassword] = useState(""); + const [passwordValid, setPasswordValid] = useState(true); const authCtx = useAuth(); const navigate = useNavigate(); @@ -32,13 +44,44 @@ export default function LoginPage() { } }, [authCtx.isLoggedIn, navigate]); + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + + // Show error messages & form validation when email or password are empty + if (!email) { + toast.error("Email address cannot be empty.", { toastId: ToastId.EmptyEmail }); + setEmailValid(false); + } + + if (!password) { + toast.error("Password cannot be empty.", { toastId: ToastId.EmptyPassword }); + setPasswordValid(false); + } + + if (email && password) { + toast.dismiss(); + await toast.promise( + callLogIn, + { + pending: "Logging in...", + }, + { toastId: ToastId.PendingRequest } + ); + } + } + async function callLogIn() { - try { - const response = await logIn(authCtx, email, password); - if (response) navigate("/editions"); - else alert("Something went wrong when login in"); - } catch (error) { - console.log(error); + const status = await logInEmail(authCtx, email, password); + toast.dismiss(); + + if (status === 200) { + navigate("/editions"); + } else if (status === 401) { + toast.error("Invalid email/password combination."); + setEmailValid(false); + setPasswordValid(false); + } else { + toast.warning("Something went wrong on our side."); } } @@ -50,18 +93,46 @@ export default function LoginPage() { - - - - Don't have an account? Ask an admin for an invite link - -
    - Log In -
    +
    + + { + setEmailValid(true); + setEmail(e.target.value); + }} + onFocus={() => setEmailValid(true)} + isInvalid={!emailValid} + /> + + + { + setPasswordValid(true); + setPassword(e.target.value); + }} + onFocus={() => setPasswordValid(true)} + isInvalid={!passwordValid} + /> + + + Don't have an account? Ask an admin for an invite link + +
    + +
    +
    diff --git a/frontend/src/views/LoginPage/styles.ts b/frontend/src/views/LoginPage/styles.ts index fe3c9f764..ee0b9db8c 100644 --- a/frontend/src/views/LoginPage/styles.ts +++ b/frontend/src/views/LoginPage/styles.ts @@ -32,13 +32,3 @@ export const VerticalDivider = styled.div` export const NoAccount = styled.div` padding-bottom: 15px; `; - -export const LoginButton = styled.button` - width: 120px; - height: 35px; - cursor: pointer; - background: var(--osoc_green); - color: white; - border: none; - border-radius: 5px; -`; diff --git a/frontend/src/views/MailOverviewPage/MailOverviewPage.tsx b/frontend/src/views/MailOverviewPage/MailOverviewPage.tsx new file mode 100644 index 000000000..6a490a8a9 --- /dev/null +++ b/frontend/src/views/MailOverviewPage/MailOverviewPage.tsx @@ -0,0 +1,305 @@ +import React, { useEffect, useState } from "react"; +import { getMailOverview, setStateRequest, StudentEmail } from "../../utils/api/mail_overview"; +import Dropdown from "react-bootstrap/Dropdown"; +import InfiniteScroll from "react-infinite-scroller"; +import { Form } from "react-bootstrap"; +import { + DropDownButtonDiv, + SearchDiv, + FilterDiv, + CenterDiv, + MessageDiv, + MailOverviewDiv, + SearchAndChangeDiv, + ClearDiv, + CustomStyledTable, +} from "./styles"; +import "../../components/Common/Buttons/buttonsStyles.css"; +import { EmailType } from "../../data/enums"; +import { useParams } from "react-router-dom"; +import { Student } from "../../data/interfaces"; +import LoadSpinner from "../../components/Common/LoadSpinner"; +import { toast } from "react-toastify"; +import SearchBar from "../../components/Common/Forms/SearchBar"; +import { CommonMultiselect } from "../../components/Common/Forms"; +import { CommonDropdownButton } from "../../components/Common/Buttons/styles"; +import { DateTd, DateTh } from "../../components/Common/Tables/styles"; +import { isReadonlyEdition } from "../../utils/logic"; +import { useAuth } from "../../contexts"; + +interface EmailRow { + email: StudentEmail; + checked: boolean; +} + +/** + * Page that shows the email status of all students, with the possibility to change the status + */ +export default function MailOverviewPage() { + const { editionId } = useParams(); + const { editions } = useAuth(); + + const [emailRows, setEmailRows] = useState([]); + const [loading, setLoading] = useState(false); + const [requestedEdition, setRequestedEdition] = useState(editionId); + const [moreEmailsAvailable, setMoreEmailsAvailable] = useState(true); // Endpoint has more emailRows available + const [page, setPage] = useState(0); + const [allSelected, setAllSelected] = useState(false); + + const [controller, setController] = useState(undefined); + + // Keep track of the set filters + const [searchTerm, setSearchTerm] = useState(""); + const [filters, setFilters] = useState([]); + const [filtersChanged, setFiltersChanged] = useState(0); + + /** + * update the table with new values + */ + async function updateMailOverview(requested: number) { + const filterChanged = requested === -1; + const requestedPage = requested === -1 ? 0 : page; + + if (loading && !filterChanged) { + return; + } + + setLoading(true); + + if (controller !== undefined) { + controller.abort(); + } + const newController = new AbortController(); + setController(newController); + + try { + const response = await getMailOverview( + editionId, + requestedPage, + searchTerm, + filters, + newController + ); + if (response !== null) { + if (response.studentEmails.length === 0 && !filterChanged) { + setMoreEmailsAvailable(false); + } else { + setMoreEmailsAvailable(true); + } + if (requestedPage === 0) { + setEmailRows( + response.studentEmails.map(email => { + return { + email: email, + checked: false, + }; + }) + ); + } else { + setEmailRows( + emailRows.concat( + response.studentEmails.map(email => { + return { + email: email, + checked: false, + }; + }) + ) + ); + } + setPage(requestedPage + 1); + } else { + setMoreEmailsAvailable(false); + } + } catch (error) { + toast.error("Failed to retrieve states"); + setMoreEmailsAvailable(false); + } + + setLoading(false); + } + + useEffect(() => { + if (editionId !== requestedEdition) { + setRequestedEdition(editionId); + refresh(); + } else { + setPage(0); + setMoreEmailsAvailable(true); + updateMailOverview(-1); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchTerm, filtersChanged, editionId]); + + function refresh() { + setEmailRows([]); + setPage(0); + setMoreEmailsAvailable(true); + updateMailOverview(-1); + } + + /** + * Keeps the selectedRows list up-to-date when a student is selected/unselected in the table + */ + function selectNewStudent(student: Student, isSelect: boolean) { + setEmailRows( + emailRows.map(row => { + if (row.email.student === student) { + row.checked = isSelect; + } + return row; + }) + ); + setAllSelected(false); + } + + function selectAll(isSelect: boolean) { + setAllSelected(isSelect); + setEmailRows( + emailRows.map(row => { + row.checked = isSelect; + return row; + }) + ); + } + + async function changeState(eventKey: string) { + const selectedStudents = emailRows + .filter(row => row.checked) + .map(row => row.email.student.studentId); + + await toast.promise(setStateRequest(eventKey, editionId, selectedStudents), { + error: "Failed to change state", + pending: "Changing state", + success: "Successfully added state", + }); + setEmailRows( + emailRows.map(row => { + row.checked = false; + return row; + }) + ); + setAllSelected(false); + refresh(); + } + + let table; + if (emailRows.length === 0) { + if (loading) { + table = ; + } else { + table = ( + + No students found. + + ); + } + } else { + table = ( + } + initialLoad={true} + useWindow={false} + getScrollParent={() => document.getElementById("root")} + > + + + + + selectAll(e.target.checked)} + checked={allSelected} + /> + + First Name + Last Name + Current State + Date + + + + {emailRows.map(row => ( + + + + selectNewStudent(row.email.student, e.target.checked) + } + checked={row.checked} + /> + + {row.email.student.firstName} + {row.email.student.lastName} + {Object.values(EmailType)[row.email.emails[0].decision]} + + {new Date(String(row.email.emails[0].date)).toLocaleString( + "nl-be" + )} + + + ))} + + + + ); + } + + return ( + + + { + setPage(0); + setSearchTerm(e.target.value); + }} + value={searchTerm} + placeholder="Search a student" + /> + + + + { + setPage(0); + setFilters(e); + setFiltersChanged(filtersChanged + 1); + }} + onSelect={e => { + setPage(0); + setFilters(e); + setFiltersChanged(filtersChanged + 1); + }} + options={Object.values(EmailType)} + /> + + {!isReadonlyEdition(editionId, editions) && ( + + + {Object.values(EmailType).map((type, index) => ( + changeState(index.toString())} + > + {type} + + ))} + + + )} + + + {table} + + ); +} diff --git a/frontend/src/views/MailOverviewPage/index.ts b/frontend/src/views/MailOverviewPage/index.ts new file mode 100644 index 000000000..b955d779b --- /dev/null +++ b/frontend/src/views/MailOverviewPage/index.ts @@ -0,0 +1 @@ +export { default } from "./MailOverviewPage"; diff --git a/frontend/src/views/MailOverviewPage/styles.ts b/frontend/src/views/MailOverviewPage/styles.ts new file mode 100644 index 000000000..11ced2aca --- /dev/null +++ b/frontend/src/views/MailOverviewPage/styles.ts @@ -0,0 +1,49 @@ +import styled from "styled-components"; +import { StyledTable } from "../../components/Common/Tables/styles"; + +export const DropDownButtonDiv = styled.div` + float: right; +`; + +export const SearchDiv = styled.div` + margin-top: 20px; + width: 14em; + display: inline-block; +`; + +export const FilterDiv = styled.div` + width: 14em; + max-width: 350px; + display: inline-block; +`; + +export const SearchAndChangeDiv = styled.div` + width: 100%; + margin-bottom: 5px; +`; + +export const CenterDiv = styled.div` + width: 100%; + margin: auto; +`; + +export const MessageDiv = styled.div` + width: fit-content; + margin: auto; +`; + +export const MailOverviewDiv = styled.div` + width: fit-content; + margin: auto; + max-width: 95%; +`; + +export const ClearDiv = styled.div` + clear: both; +`; + +export const CustomStyledTable = styled(StyledTable)` + min-width: fit-content; + width: 60em; + max-width: 100%; +`; diff --git a/frontend/src/views/OAuth/GitHubOAuth.tsx b/frontend/src/views/OAuth/GitHubOAuth.tsx new file mode 100644 index 000000000..ad3b84f33 --- /dev/null +++ b/frontend/src/views/OAuth/GitHubOAuth.tsx @@ -0,0 +1,66 @@ +/** + * Page displayed while GitHub auth is validating your response code + */ +import { Navigate, useSearchParams } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { logInGitHub } from "../../utils/api/login"; +import { useAuth } from "../../contexts"; +import { PageContainer, CenterText } from "../../App.styles"; + +export default function GitHubOAuth() { + const authCtx = useAuth(); + const [searchParams] = useSearchParams(); + const [loading, setLoading] = useState(true); + const [showError, setShowError] = useState(false); + + useEffect(() => { + let unmounted = false; + + async function tryLogIn() { + // No code in the query parameters + if (!searchParams.has("code")) { + setLoading(false); + setShowError(true); + return; + } + + // Fix memory leak: only send API call if not yet unmounted + if (!unmounted) { + const response = await logInGitHub(authCtx, searchParams.get("code")!); + setLoading(false); + if (!response) { + setShowError(true); + } + } + } + + tryLogIn(); + + return () => { + unmounted = true; + }; + // We don't want to update when the "loading" and "authCtx" dependencies change + // because this creates an infinite loop, so we ignore the eslint warning + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchParams]); + + if (showError) { + return ( + + +

    Invalid GitHub credentials

    +
    +
    + ); + } + + if (loading) { + return ( + + Authenticating... + + ); + } + + return ; +} diff --git a/frontend/src/views/OAuth/index.ts b/frontend/src/views/OAuth/index.ts new file mode 100644 index 000000000..6e2c3f17b --- /dev/null +++ b/frontend/src/views/OAuth/index.ts @@ -0,0 +1 @@ +export { default as GitHubOAuth } from "./GitHubOAuth"; diff --git a/frontend/src/views/Registration/RedirectPage/RedirectPage.tsx b/frontend/src/views/Registration/RedirectPage/RedirectPage.tsx new file mode 100644 index 000000000..a2f11d914 --- /dev/null +++ b/frontend/src/views/Registration/RedirectPage/RedirectPage.tsx @@ -0,0 +1,95 @@ +import { useSearchParams } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { getRegisterState } from "../../../utils/session-storage"; +import { registerGithub } from "../../../utils/api/register"; +import axios from "axios"; +import PendingPage from "../../PendingPage"; +import { CenterText, PageContainer } from "../../../App.styles"; +import { AlreadyRegistered } from "../../../components/RegisterComponents"; + +/** + * Page where users end up after using an OAuth application + */ +export default function RedirectPage() { + const [searchParams] = useSearchParams(); + const [alreadyRegistered, setAlreadyRegistered] = useState(false); + const [showErrorMessage, setShowErrorMessage] = useState(false); + const [loading, setLoading] = useState(false); + + useEffect(() => { + // Check if all parameters are present + const requiredParams = ["edition", "uuid", "provider", "state", "code"]; + + if (requiredParams.some(key => !searchParams.has(key))) { + setShowErrorMessage(true); + return; + } + + // Check if state matches + if (!(getRegisterState() === searchParams.get("state"))) { + setShowErrorMessage(true); + return; + } + + async function doGithubRegister() { + // These can never be null as that's checked above, so the '!' operators are safe but + // required to make ESLint happy + setLoading(true); + + try { + await registerGithub( + searchParams.get("edition")!, + searchParams.get("uuid")!, + searchParams.get("code")! + ); + + setShowErrorMessage(false); + } catch (e) { + if (axios.isAxiosError(e)) { + if (e.response && e.response.status === 409) { + setAlreadyRegistered(true); + } else { + setShowErrorMessage(true); + console.log(e.response); + } + } + } + + setLoading(false); + } + + doGithubRegister(); + }, [searchParams]); // searchParams never updates, but it's a state hook so this is required + + if (alreadyRegistered) { + return ( + + + + ); + } + + if (showErrorMessage) { + return ( + + +

    Uh-oh!

    +
    + That doesn't look like a valid URI. +
    + Did you fiddle with something? +
    +
    + ); + } + + if (loading) { + return ( + + Registering... + + ); + } + + return ; +} diff --git a/frontend/src/views/Registration/RedirectPage/index.ts b/frontend/src/views/Registration/RedirectPage/index.ts new file mode 100644 index 000000000..4d7e273d6 --- /dev/null +++ b/frontend/src/views/Registration/RedirectPage/index.ts @@ -0,0 +1 @@ +export { default } from "./RedirectPage"; diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/Registration/RegisterPage/RegisterPage.tsx similarity index 73% rename from frontend/src/views/RegisterPage/RegisterPage.tsx rename to frontend/src/views/Registration/RegisterPage/RegisterPage.tsx index dcc831cdc..3588a87ad 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/Registration/RegisterPage/RegisterPage.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; -import { register } from "../../utils/api/register"; -import { validateRegistrationUrl } from "../../utils/api"; +import { register } from "../../../utils/api/register"; +import { validateRegistrationUrl } from "../../../utils/api"; import { Email, @@ -12,17 +12,23 @@ import { SocialButtons, InfoText, BadInviteLink, -} from "../../components/RegisterComponents"; + AlreadyRegistered, +} from "../../../components/RegisterComponents"; import { RegisterFormContainer, Or, RegisterButton } from "./styles"; -import { decodeRegistrationLink } from "../../utils/logic/registration"; -import PendingPage from "../PendingPage"; +import { decodeRegistrationLink } from "../../../utils/logic"; +import PendingPage from "../../PendingPage"; +import { useAuth } from "../../../contexts"; +import { PageContainer } from "../../../App.styles"; +import { LoadSpinner } from "../../../components/Common"; /** * Page where a user can register a new account. If the uuid in the url is invalid, * this renders the [[BadInviteLink]] component instead. */ export default function RegisterPage() { + const { isLoggedIn } = useAuth(); + const [validating, setValidating] = useState(true); const [validUuid, setValidUuid] = useState(false); const [pending, setPending] = useState(false); const params = useParams(); @@ -41,6 +47,8 @@ export default function RegisterPage() { if (response) { setValidUuid(true); } + + setValidating(false); } } if (!validUuid) { @@ -71,6 +79,24 @@ export default function RegisterPage() { } } + // URL is still being validated + if (validating) { + return ( + + + + ); + } + + // User is currently logged in, show them a message instead + if (isLoggedIn) { + return ( + + + + ); + } + if (pending) { return ; } @@ -84,7 +110,7 @@ export default function RegisterPage() {
    - + or diff --git a/frontend/src/views/RegisterPage/index.ts b/frontend/src/views/Registration/RegisterPage/index.ts similarity index 100% rename from frontend/src/views/RegisterPage/index.ts rename to frontend/src/views/Registration/RegisterPage/index.ts diff --git a/frontend/src/views/RegisterPage/styles.ts b/frontend/src/views/Registration/RegisterPage/styles.ts similarity index 100% rename from frontend/src/views/RegisterPage/styles.ts rename to frontend/src/views/Registration/RegisterPage/styles.ts diff --git a/frontend/src/views/Registration/index.ts b/frontend/src/views/Registration/index.ts new file mode 100644 index 000000000..e21cd3336 --- /dev/null +++ b/frontend/src/views/Registration/index.ts @@ -0,0 +1,2 @@ +export { default as RedirectPage } from "./RedirectPage"; +export { default as RegisterPage } from "./RegisterPage"; diff --git a/frontend/src/views/StudentInfoPage/StudentInfoPage.tsx b/frontend/src/views/StudentInfoPage/StudentInfoPage.tsx new file mode 100644 index 000000000..ad492aa61 --- /dev/null +++ b/frontend/src/views/StudentInfoPage/StudentInfoPage.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import StudentInfo from "../../components/StudentInfoComponents/StudentInfo"; + +/** + * @returns the detailed page of a student. Here you can make a suggestion and admins + * can make definitive decisions on students. You can also remove the currently selected student. + */ +function StudentInfoPage() { + const params = useParams(); + const studentId = params.id; + const editionId = params.editionId; + + const navigate = useNavigate(); + + if (studentId === undefined || (isNaN(+studentId) && editionId === undefined)) { + navigate("/404-not-found"); + } + + return ; +} + +export default StudentInfoPage; diff --git a/frontend/src/views/StudentInfoPage/index.ts b/frontend/src/views/StudentInfoPage/index.ts new file mode 100644 index 000000000..5e25cd7fd --- /dev/null +++ b/frontend/src/views/StudentInfoPage/index.ts @@ -0,0 +1 @@ +export { default } from "./StudentInfoPage"; diff --git a/frontend/src/views/StudentMailHistoryPage/StudentMailHistoryPage.tsx b/frontend/src/views/StudentMailHistoryPage/StudentMailHistoryPage.tsx new file mode 100644 index 000000000..9ad67ca72 --- /dev/null +++ b/frontend/src/views/StudentMailHistoryPage/StudentMailHistoryPage.tsx @@ -0,0 +1,124 @@ +import React, { useEffect, useState } from "react"; +import { + BackButtonDiv, + ButtonDiv, + CenterDiv, + CustomDropdownButton, + NameDiv, + TableDiv, +} from "./styles"; +import { getEmails } from "../../utils/api/student_email_history"; +import { Email, Student } from "../../data/interfaces"; +import { EmailType } from "../../data/enums"; +import { useNavigate, useParams } from "react-router-dom"; +import { MessageDiv } from "../MailOverviewPage/styles"; +import { DateTd, DateTh, StyledTable } from "../../components/Common/Tables/styles"; +import { LoadSpinner } from "../../components/Common"; +import BackButton from "../../components/Common/Buttons/BackButton"; +import Dropdown from "react-bootstrap/Dropdown"; +import { toast } from "react-toastify"; +import { setStateRequest } from "../../utils/api/mail_overview"; +import { isReadonlyEdition } from "../../utils/logic"; +import { useAuth } from "../../contexts"; + +/** + * Page that shows the email history of a student in a table + */ +export default function StudentMailHistoryPage() { + const [emails, setEmails] = useState([]); + const [gotEmails, setGotEmails] = useState(false); + const [student, setStudent] = useState(); + + const { editionId, id } = useParams(); + const { editions } = useAuth(); + const navigate = useNavigate(); + + async function getData() { + setEmails([]); + setGotEmails(false); + try { + const response = await getEmails(editionId, id); + setEmails(response.emails); + setStudent(response.student); + setGotEmails(true); + } catch (exception) { + console.log(exception); + } + } + + useEffect(() => { + getData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [editionId, id]); + + async function changeState(eventKey: string) { + await toast.promise(setStateRequest(eventKey, editionId, [student!.studentId]), { + error: "Failed to change state", + pending: "Changing state", + success: "Successfully added state", + }); + await getData(); + } + + if (!gotEmails) { + return ; + } + + let emailtable; + if (emails.length === 0) { + emailtable = No states found.; + } else { + emailtable = ( + + + + + State + Date + + + + {emails.map(d => ( + + {Object.values(EmailType)[d.decision]} + {new Date(String(d.date)).toLocaleString("nl-be")} + + ))} + + + + ); + } + + return ( +
    + + navigate("/editions/" + editionId + "/students/" + id)} + label=" Student details" + /> + + + +

    {student?.firstName + " " + student?.lastName}

    +
    + {!isReadonlyEdition(editionId, editions) && ( + + + {Object.values(EmailType).map((type, index) => ( + changeState(index.toString())} + > + {type} + + ))} + + + )} + {emailtable} +
    +
    + ); +} diff --git a/frontend/src/views/StudentMailHistoryPage/index.ts b/frontend/src/views/StudentMailHistoryPage/index.ts new file mode 100644 index 000000000..515a65842 --- /dev/null +++ b/frontend/src/views/StudentMailHistoryPage/index.ts @@ -0,0 +1 @@ +export { default } from "./StudentMailHistoryPage"; diff --git a/frontend/src/views/StudentMailHistoryPage/styles.ts b/frontend/src/views/StudentMailHistoryPage/styles.ts new file mode 100644 index 000000000..a648ba1d9 --- /dev/null +++ b/frontend/src/views/StudentMailHistoryPage/styles.ts @@ -0,0 +1,40 @@ +import styled from "styled-components"; +import { CommonDropdownButton } from "../../components/Common/Buttons/styles"; + +export const TableDiv = styled.div` + width: 100%; + min-width: fit-content; + margin: auto; +`; + +export const BackButtonDiv = styled.div` + margin-left: 2em; + float: left; +`; + +export const NameDiv = styled.div` + overflow: hidden; + width: fit-content; + margin: auto; +`; + +export const ButtonDiv = styled.div` + width: fit-content; + margin-bottom: 5px; + margin-top: 10px; +`; + +export const CenterDiv = styled.div` + width: 45em; + min-width: fit-content; + max-width: 95%; + margin: 2em auto; +`; + +export const CustomDropdownButton = styled(CommonDropdownButton)` + width: 11em; + + & > Button { + width: 11em; + } +`; diff --git a/frontend/src/views/StudentsPage/StudentsPage.tsx b/frontend/src/views/StudentsPage/StudentsPage.tsx index 5a158d93d..afeca725a 100644 --- a/frontend/src/views/StudentsPage/StudentsPage.tsx +++ b/frontend/src/views/StudentsPage/StudentsPage.tsx @@ -1,8 +1,15 @@ -import React from "react"; -import "./StudentsPage.css"; +import { DragDropContext } from "react-beautiful-dnd"; +import { StudentListFilters } from "../../components/StudentsComponents"; -function Students() { - return
    This is the students page
    ; +/** + * @returns Page where admins and coaches can filter on students. + */ +function StudentsPage() { + return ( + {}}> + + + ); } -export default Students; +export default StudentsPage; diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index 5e709bb0f..e62dda825 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -1,86 +1,120 @@ import React, { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; -import { UsersPageDiv, UsersHeader } from "./styles"; +import { useNavigate, useParams } from "react-router-dom"; import { Coaches } from "../../components/UsersComponents/Coaches"; import { InviteUser } from "../../components/UsersComponents/InviteUser"; import { PendingRequests } from "../../components/UsersComponents/Requests"; import { User } from "../../utils/api/users/users"; import { getCoaches } from "../../utils/api/users/coaches"; +import { toast } from "react-toastify"; /** * Page for admins to manage coach and admin settings. */ function UsersPage() { + const params = useParams(); + // Note: The coaches are not in the coaches component because accepting a request needs to refresh the coaches list. + const [allCoaches, setAllCoaches] = useState([]); const [coaches, setCoaches] = useState([]); // All coaches from the selected edition - const [gettingData, setGettingData] = useState(false); // Waiting for data (used for spinner) - const [gotData, setGotData] = useState(false); // Received data - const [error, setError] = useState(""); // Error message + const [loading, setLoading] = useState(false); // Waiting for data (used for spinner) const [moreCoachesAvailable, setMoreCoachesAvailable] = useState(true); // Endpoint has more coaches available + const [requestedEdition, setRequestedEdition] = useState(params.editionId); + const [allCoachesFetched, setAllCoachesFetched] = useState(false); const [searchTerm, setSearchTerm] = useState(""); // The word set in filter for coachlist + const [page, setPage] = useState(0); // The next page to request - const params = useParams(); + const [controller, setController] = useState(undefined); + + const navigate = useNavigate(); /** - * Request a page from the list of coaches. - * An optional filter can be used to filter the username. - * If the filter is not used, the string saved in the "searchTerm" state will be used. - * @param page The page to load. - * @param filter Optional string to filter username. + * Request the next page from the list of coaches. + * The set searchterm will be used. */ - async function getCoachesData(page: number, filter: string | undefined = undefined) { - if (filter === undefined) { - filter = searchTerm; + async function getCoachesData(requested: number, reset: boolean) { + const filterChanged = requested === -1; + const requestedPage = requested === -1 ? 0 : page; + + if (loading && !filterChanged) { + return; + } + + if (allCoachesFetched && !reset) { + setCoaches( + allCoaches.filter(coach => + coach.name.toUpperCase().includes(searchTerm.toUpperCase()) + ) + ); + setMoreCoachesAvailable(false); + return; } - setGettingData(true); - setError(""); - try { - const coachResponse = await getCoaches(params.editionId as string, filter, page); - if (coachResponse.users.length === 0) { + + setLoading(true); + + if (controller !== undefined) { + controller.abort(); + } + const newController = new AbortController(); + setController(newController); + + const response = await toast.promise( + getCoaches(params.editionId as string, searchTerm, requestedPage, newController), + { error: "Failed to retrieve coaches" } + ); + + if (response !== null) { + if (response.users.length === 0 && !filterChanged) { setMoreCoachesAvailable(false); + } else { + setMoreCoachesAvailable(true); } - if (page === 0) { - setCoaches(coachResponse.users); + if (requestedPage === 0 || filterChanged) { + setCoaches(response.users); } else { - setCoaches(coaches.concat(coachResponse.users)); + setCoaches(coaches.concat(response.users)); + } + + if (searchTerm === "") { + if (response.users.length === 0) { + setAllCoachesFetched(true); + } + if (requestedPage === 0) { + setAllCoaches(response.users); + } else { + setAllCoaches(allCoaches.concat(response.users)); + } } - setGotData(true); - setGettingData(false); - } catch (exception) { - setError("Oops, something went wrong..."); - setGettingData(false); + setPage(page + 1); + } else { + setMoreCoachesAvailable(false); } + + setLoading(false); } useEffect(() => { - if (!gotData && !gettingData && !error) { - getCoachesData(0); + if (params.editionId !== requestedEdition) { + setRequestedEdition(params.editionId); + refreshCoaches(); + } else { + setPage(0); + setMoreCoachesAvailable(true); + getCoachesData(-1, false); } - }); - - /** - * Set the searchTerm and request the first page with this filter. - * The current list of coaches will be resetted. - * @param searchTerm The string to filter coaches with by username. - */ - function filterCoachesData(searchTerm: string) { - setGotData(false); - setSearchTerm(searchTerm); - setCoaches([]); - setMoreCoachesAvailable(true); - getCoachesData(0, searchTerm); - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchTerm, params.editionId]); /** * Reset the list of coaches and get the first page. - * Used when a new coach is added. + * Used when a new coach is added, or when the edition is changed. */ function refreshCoaches() { - setGotData(false); setCoaches([]); + setPage(0); + setAllCoachesFetched(false); setMoreCoachesAvailable(true); - getCoachesData(0); + getCoachesData(-1, true); } /** @@ -93,35 +127,34 @@ function UsersPage() { return object !== coach; }) ); + setAllCoaches( + allCoaches.filter(object => { + return object !== coach; + }) + ); } if (params.editionId === undefined) { - // If this happens, User should be redirected to error page - return
    Error
    ; + navigate("/404-not-found"); + return null; } else { return ( - -
    - -

    Manage coaches from {params.editionId}

    -
    -
    +
    - +
    ); } } diff --git a/frontend/src/views/UsersPage/styles.ts b/frontend/src/views/UsersPage/styles.ts deleted file mode 100644 index 02aaf4d40..000000000 --- a/frontend/src/views/UsersPage/styles.ts +++ /dev/null @@ -1,10 +0,0 @@ -import styled from "styled-components"; - -export const UsersPageDiv = styled.div``; - -export const UsersHeader = styled.div` - padding-left: 10px; - margin-top: 10px; - float: left; - display: inline-block; -`; diff --git a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx index d90de2bf5..ccab9b2dd 100644 --- a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx +++ b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx @@ -1,5 +1,6 @@ import { useEffect } from "react"; +import { LoadSpinner } from "../../components/Common"; import { validateBearerToken } from "../../utils/api/auth"; import { logIn, logOut, useAuth } from "../../contexts/auth-context"; import { getAccessToken, getRefreshToken } from "../../utils/local-storage"; @@ -39,7 +40,7 @@ export default function VerifyingTokenPage() { // This will be replaced later on return (
    -

    Loading...

    +
    ); } diff --git a/frontend/src/views/index.ts b/frontend/src/views/index.ts index 0aa72e445..c06c220b2 100644 --- a/frontend/src/views/index.ts +++ b/frontend/src/views/index.ts @@ -1,11 +1,14 @@ export * as Errors from "./errors"; +export * as Registration from "./Registration"; export { default as LoginPage } from "./LoginPage"; export { default as EditionsPage } from "./EditionsPage"; export { default as CreateEditionPage } from "./CreateEditionPage"; export { default as PendingPage } from "./PendingPage"; export { ProjectsPage, ProjectDetailPage, CreateProjectPage } from "./projectViews"; -export { default as RegisterPage } from "./RegisterPage"; export { default as StudentsPage } from "./StudentsPage"; +export { default as StudentInfoPage } from "./StudentInfoPage"; export { default as UsersPage } from "./UsersPage"; export { default as AdminsPage } from "./AdminsPage"; export { default as VerifyingTokenPage } from "./VerifyingTokenPage"; +export { default as StudentMailHistoryPage } from "./StudentMailHistoryPage"; +export { default as MailOverviewPage } from "./MailOverviewPage"; diff --git a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx index 33d800399..1857065dd 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx +++ b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx @@ -1,12 +1,17 @@ -import { CreateProjectContainer, CreateButton, Label } from "./styles"; +import { + CreateProjectContainer, + Label, + CenterContainer, + Center, + CancelButton, + CenterTitle, +} from "./styles"; import { createProject } from "../../../utils/api/projects"; import { useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { GoBack } from "../ProjectDetailPage/styles"; import { BiArrowBack } from "react-icons/bi"; import { NameInput, - NumberOfStudentsInput, CoachInput, SkillInput, PartnerInput, @@ -16,93 +21,135 @@ import { } from "../../../components/ProjectsComponents/CreateProjectComponents"; import { SkillProject } from "../../../data/interfaces/projects"; import { User } from "../../../utils/api/users/users"; - +import { toast } from "react-toastify"; +import { createProjectRole } from "../../../utils/api/projectRoles"; +import { CreateButton } from "../../../components/Common/Buttons"; +import InfoUrl from "../../../components/ProjectsComponents/CreateProjectComponents/InputFields/InfoUrl"; /** * React component of the create project page. * @returns The create project page. */ + export default function CreateProjectPage() { - const [name, setName] = useState(""); - const [numberOfStudents, setNumberOfStudents] = useState(1); + const [name, setName] = useState(""); // State for project name + + const [infoUrl, setInfoUrl] = useState(""); // State for info link - // States for coaches const [coach, setCoach] = useState(""); - const [coaches, setCoaches] = useState([]); + const [coaches, setCoaches] = useState([]); // States for coaches - // States for skills const [skill, setSkill] = useState(""); - const [skills, setSkills] = useState([]); + const [projectSkills, setProjectSkills] = useState([]); // States for skills - // States for partners const [partner, setPartner] = useState(""); - const [partners, setPartners] = useState([]); + const [partners, setPartners] = useState([]); // States for partners const navigate = useNavigate(); - const params = useParams(); const editionId = params.editionId!; return ( - - navigate("/editions/" + editionId + "/projects/")}> - - Cancel - -

    New Project

    - - - - - - - - - - - - - - - - - - - - { - const coachIds: number[] = []; - coaches.forEach(coachToAdd => { - coachIds.push(coachToAdd.userId); - }); - - const response = await createProject( - editionId, - name, - numberOfStudents!, - [], // Empty skills for now TODO - partners, - coachIds - ); - if (response) { - navigate("/editions/" + editionId + "/projects/"); - } else alert("Something went wrong :("); - }} - > - Create Project - -
    + + + +

    New Project

    +
    + + + + + + + + + + + + + + + + + +
    + navigate("/editions/" + editionId + "/projects/")}> + + Cancel + + +
    +
    +
    ); + + async function makeProject() { + if (name === "") { + toast.error("Project name must be filled in", { + toastId: "createProjectNoName", + }); + return; + } + + if (infoUrl !== "" && !infoUrl.startsWith("https://") && !infoUrl.startsWith("http://")) { + toast.error("InfoUrl should start with https:// or http://", { + toastId: "createProjectBadUrl", + }); + return; + } + + let badSkill = false; + projectSkills.forEach(projectSkill => { + if (isNaN(projectSkill.slots)) { + badSkill = true; + toast.error(projectSkill.skill.name + " is missing the amount of students", { + toastId: "invalidSkill" + projectSkill.skill.name, + }); + } + }); + if (badSkill) return; + + const coachIds: number[] = []; + coaches.forEach(coachToAdd => { + coachIds.push(coachToAdd.userId); + }); + const response = await createProject(editionId, name, infoUrl, partners, coachIds); + + if (response) { + await toast.promise(addProjectRoles(response.projectId), { + pending: "Creating project", + success: "Successfully created project", + error: "Something went wrong", + }); + navigate("/editions/" + editionId + "/projects/" + response.projectId); + } else toast.error("Something went wrong"); + } + + async function addProjectRoles(projectId: number) { + // Use a for loop or else await won't work as intended + for (const projectSkill of projectSkills) { + const addedSkill = await createProjectRole( + editionId, + projectId.toString(), + projectSkill.skill.skillId, + projectSkill.description, + projectSkill.slots + ); + if (!addedSkill) toast.error("Couldn't add skill" + projectSkill.skill.name); + } + } } diff --git a/frontend/src/views/projectViews/CreateProjectPage/styles.ts b/frontend/src/views/projectViews/CreateProjectPage/styles.ts index 6cbd8c1c3..ee035993e 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/styles.ts +++ b/frontend/src/views/projectViews/CreateProjectPage/styles.ts @@ -1,7 +1,12 @@ import styled from "styled-components"; +export const CenterContainer = styled.div` + width: 100%; +`; + export const CreateProjectContainer = styled.div` - margin: 20px; + width: fit-content; + margin: 20px auto; `; export const Input = styled.input` @@ -33,12 +38,35 @@ export const RemoveButton = styled.button` align-items: center; `; +export const Center = styled.div` + display: flex; + align-items: center; + vertical-align: middle; + margin: 15px auto; + width: fit-content; +`; + +export const CenterTitle = styled.div` + margin: 5px auto; + width: fit-content; +`; + export const CreateButton = styled.button` padding: 5px 10px; background-color: #44dba4; color: white; border: none; - margin-top: 30px; + border-radius: 5px; +`; + +export const CancelButton = styled.button` + display: flex; + align-items: center; + padding: 7px 12px; + margin-right: 5px; + background-color: #131329; + color: white; + border: none; border-radius: 5px; `; diff --git a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx index 9c0184eef..290ee3e4e 100644 --- a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx +++ b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx @@ -1,107 +1,304 @@ import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { Project } from "../../../data/interfaces"; - -import { getProject } from "../../../utils/api/projects"; +import { DragDropContext, DropResult } from "react-beautiful-dnd"; +import { + Project, + CreateProject as EditProject, + AddStudentRole, +} from "../../../data/interfaces/projects"; +import projectToEditProject from "../../../utils/logic/project"; +import { deleteProject, getProject, patchProject } from "../../../utils/api/projects"; +import { useAuth, useSockets } from "../../../contexts"; +import { BsPersonFill } from "react-icons/bs"; import { - GoBack, ProjectContainer, - Client, - ClientContainer, + ClientsContainer, NumberOfStudents, - Title, + ProjectPageContainer, + MoreInfoLink, } from "./styles"; - -import { BiArrowBack } from "react-icons/bi"; -import { BsPersonFill } from "react-icons/bs"; - -import { StudentPlace } from "../../../data/interfaces/projects"; -import { StudentPlaceholder } from "../../../components/ProjectsComponents"; +import ConfirmDelete from "../../../components/ProjectsComponents/ConfirmDelete"; import { - CoachContainer, - CoachesContainer, - CoachText, -} from "../../../components/ProjectsComponents/ProjectCard/styles"; + TitleAndEdit, + ProjectRoles, + ProjectCoaches, + ProjectPartners, + AddStudentModal, +} from "../../../components/ProjectDetailComponents"; +import { addStudentToProject, deleteStudentFromProject } from "../../../utils/api/projectStudents"; +import { toast } from "react-toastify"; +import { StudentListFilters } from "../../../components/StudentsComponents"; +import { CreateButton } from "../../../components/Common/Buttons"; +import BackButton from "../../../components/Common/Buttons/BackButton"; +import { EventType, RequestMethod, WebSocketEvent } from "../../../data/interfaces/websockets"; + +// Types of events accepted by this websocket +const wsEventTypes = [EventType.PROJECT, EventType.PROJECT_ROLE, EventType.PROJECT_ROLE_SUGGESTION]; /** * @returns the detailed page of a project. Here you can add or remove students from the project. */ export default function ProjectDetailPage() { + const { socket } = useSockets(); + const params = useParams(); const projectId = parseInt(params.projectId!); const editionId = params.editionId!; const [project, setProject] = useState(); + const [editedProject, setEditedProject] = useState(); const [gotProject, setGotProject] = useState(false); + const [editing, setEditing] = useState(false); + const [studentAmount, setStudentAmount] = useState(0); + const [assignedAmount, setAssignedAmount] = useState(0); const navigate = useNavigate(); + const { role } = useAuth(); + + // WebSocket listener + useEffect(() => { + function listener(event: MessageEvent) { + const data = JSON.parse(event.data) as WebSocketEvent; - const [students, setStudents] = useState([]); + // Ignore events that aren't targeted towards projects + if (!wsEventTypes.includes(data.eventType)) return; + const idString = projectId.toString(); + + // Ignore events targeted towards other projects + if (data.pathIds.projectId !== idString) return; + + // This project was deleted + if (data.method === RequestMethod.DELETE) { + if (data.eventType === EventType.PROJECT) { + toast.info("This project was deleted by an admin."); + navigate(`/editions/${editionId}/projects`); + return; + } + } + + // Project was edited in some way (either a PATCH or adding/deleting suggestions) + // By setting this one to False we force the other useEffect + // to fetch the project again + setGotProject(false); + } + + socket?.addEventListener("message", listener); + + function removeListener() { + if (socket) { + socket.removeEventListener("message", listener); + } + } + + return removeListener; + }, [socket, editionId, projectId, navigate]); + + // Get project details useEffect(() => { - async function callProjects() { + async function callProjects(): Promise { if (projectId) { setGotProject(true); const response = await getProject(editionId, projectId); if (response) { setProject(response); - - // TODO - // Generate student data - const studentsTemplate: StudentPlace[] = []; - for (let i = 0; i < response.numberOfStudents; i++) { - const student: StudentPlace = { - available: i % 2 === 0, - name: i % 2 === 0 ? undefined : "Tom", - skill: "Frontend", - }; - studentsTemplate.push(student); - } - setStudents(studentsTemplate); + setEditedProject(response); + let countStudents = 0; + let countAssigned = 0; + response.projectRoles.forEach(projectRole => { + countStudents += projectRole.slots; + countAssigned += projectRole.suggestions.length; + }); + setStudentAmount(countStudents); + setAssignedAmount(countAssigned); } else navigate("/404-not-found"); } } if (!gotProject) { callProjects(); } - }, [editionId, gotProject, navigate, projectId]); - if (!project) return null; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [editionId, gotProject, projectId]); + + // Used for the delete modal. + const [showDeleteModal, setShowDeleteModal] = useState(false); + const handleDeleteClose = () => setShowDeleteModal(false); + const handleDeleteShow = () => setShowDeleteModal(true); + const handleDelete = () => { + // What to do when deleting a project. + toast.promise( + deleteProject(editionId, project!.projectId), + { + pending: "Deleting project", + success: "Successfully deleted project", + error: "Something went wrong", + }, + { toastId: "deleteProject" } + ); + setShowDeleteModal(false); + navigate("/editions/" + editionId + "/projects/"); + }; + + // Used for the Add modal. + const [showAddModal, setShowAddModal] = useState(false); + const [resultModal, setResult] = useState(); + const handleAddClose = () => setShowAddModal(false); + const handleAddShow = ( + projectRoleId: string, + studentId: string, + switchProjectRoleId: string | undefined + ) => { + setResult({ + projectRoleId: projectRoleId, + studentId: studentId, + switchProjectRoleId: switchProjectRoleId, + }); + setShowAddModal(true); + }; + + const handleAdd = async (motivation: string, addStudent: AddStudentRole) => { + setShowAddModal(false); + if (addStudent.switchProjectRoleId) { + await deleteStudentFromProject( + editionId, + projectId.toString(), + addStudent.switchProjectRoleId, + addStudent.studentId + ); + } + await toast.promise( + addStudentToProject( + editionId, + projectId.toString(), + addStudent.projectRoleId, + addStudent.studentId, + motivation + ), + { + pending: "Adding student", + success: "Successfully added student", + error: "Something went wrong", + }, + { toastId: "addStudentToProject" } + ); + setGotProject(false); + }; + + async function editProject() { + const newProject: EditProject = projectToEditProject(editedProject!); + if (newProject.name === "") { + toast.error("Project name must be filled in", { + toastId: "createProjectNoName", + }); + return; + } + await toast.promise( + patchProject( + editionId, + projectId, + newProject!.name, + newProject.info_url || null, + newProject!.partners, + newProject!.coaches + ), + { + pending: "Updating project", + success: "Successfully updated project", + error: "Something went wrong", + }, + { toastId: "UpdateProject" } + ); + setGotProject(false); + } + if (!project || !editedProject) return null; return ( -
    - - navigate("/editions/" + editionId + "/projects/")}> - - Overview - - - {project.name} - - - {project.partners.map((element, _index) => ( - {element.name} - ))} - - {project.numberOfStudents} - - - - - - {project.coaches.map((element, _index) => ( - - {element.name} - - ))} - - -
    - {students.map((element: StudentPlace, _index) => ( - - ))} -
    -
    -
    + onDragDrop(result)}> + + + + + navigate("/editions/" + editionId + "/projects/")} + label="Overview" + /> + + + + + + + + {assignedAmount + " / " + studentAmount} + + + + + + + {project.infoUrl !== null && ( + + window.open(project.infoUrl!)} + /> + + )} + + + + + + ); + + async function onDragDrop(result: DropResult) { + const { source, destination, draggableId } = result; + if (destination) { + if (source.droppableId === "students") { + handleAddShow(destination.droppableId, draggableId, undefined); + } else if (source.droppableId !== destination.droppableId) { + handleAddShow( + destination.droppableId, + draggableId.substring(0, draggableId.length - source.droppableId.length), + source.droppableId + ); + } + } + } } diff --git a/frontend/src/views/projectViews/ProjectDetailPage/styles.ts b/frontend/src/views/projectViews/ProjectDetailPage/styles.ts index 914cfd91b..0b7d28a8e 100644 --- a/frontend/src/views/projectViews/ProjectDetailPage/styles.ts +++ b/frontend/src/views/projectViews/ProjectDetailPage/styles.ts @@ -1,37 +1,32 @@ import styled from "styled-components"; -export const ProjectContainer = styled.div` - margin: 20px; -`; - -export const GoBack = styled.div` +export const ProjectPageContainer = styled.div` display: flex; - align-items: center; - margin-bottom: 5px; - - :hover { - cursor: pointer; - } + height: 100vh; `; -export const Title = styled.h2` - text-overflow: ellipsis; - overflow: hidden; +export const ProjectContainer = styled.div` + width: 100%; + margin: 20px; + border: 5px; + overflow: auto; `; -export const ClientContainer = styled.div` +export const ClientsContainer = styled.div` display: flex; align-items: center; - color: lightgray; - overflow-x: auto; -`; - -export const Client = styled.h5` - margin-right: 1%; + justify-content: space-between; + margin-top: 10px; + margin-right: 20px; + max-width: 100%; `; export const NumberOfStudents = styled.div` display: flex; align-items: center; - margin-bottom: 4px; +`; + +export const MoreInfoLink = styled.div` + margin-top: 5px; + margin-bottom: 20px; `; diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index 1fe9daae8..0a5b6bcf2 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -1,30 +1,37 @@ import { useEffect, useState } from "react"; -import { getProjects } from "../../../utils/api/projects"; -import { ProjectCard, LoadSpinner } from "../../../components/ProjectsComponents"; -import { - CardsGrid, - CreateButton, - SearchButton, - SearchField, - OwnProject, - ProjectsContainer, - LoadMoreContainer, - LoadMoreButton, -} from "./styles"; +import { getProject, getProjects } from "../../../utils/api/projects"; +import { ControlContainer, OwnProject, SearchFieldDiv } from "./styles"; import { Project } from "../../../data/interfaces"; +import ProjectTable from "../../../components/ProjectsComponents/ProjectTable"; import { useNavigate, useParams } from "react-router-dom"; -import InfiniteScroll from "react-infinite-scroller"; -import { useAuth } from "../../../contexts"; +import { useAuth, useSockets } from "../../../contexts"; + import { Role } from "../../../data/enums"; +import ConflictsButton from "../../../components/ProjectsComponents/Conflicts/ConflictsButton"; +import { EventType, RequestMethod, WebSocketEvent } from "../../../data/interfaces/websockets"; +import { isReadonlyEdition } from "../../../utils/logic"; +import { toast } from "react-toastify"; +import { CreateButton } from "../../../components/Common/Buttons"; +import { SearchBar } from "../../../components/Common/Forms"; + +// Types of events accepted by this websocket +const wsEventTypes = [EventType.PROJECT, EventType.PROJECT_ROLE, EventType.PROJECT_ROLE_SUGGESTION]; + /** * @returns The projects overview page where you can see all the projects. * You can filter on your own projects or filter on project name. */ export default function ProjectPage() { + const params = useParams(); + + const [allProjects, setAllProjects] = useState([]); const [projects, setProjects] = useState([]); - const [gotProjects, setGotProjects] = useState(false); const [loading, setLoading] = useState(false); + const [requestedEdition, setRequestedEdition] = useState(params.editionId); const [moreProjectsAvailable, setMoreProjectsAvailable] = useState(true); // Endpoint has more coaches available + const [allProjectsFetched, setAllProjectsFetched] = useState(false); + + const [controller, setController] = useState(undefined); // Keep track of the set filters const [searchString, setSearchString] = useState(""); @@ -33,112 +40,203 @@ export default function ProjectPage() { const navigate = useNavigate(); const [page, setPage] = useState(0); - const params = useParams(); const editionId = params.editionId!; - const { role } = useAuth(); + const { role, editions, userId } = useAuth(); + const { socket } = useSockets(); /** * Used to fetch the projects */ - async function callProjects(newPage: number) { - if (loading) return; + async function loadProjects(requested: number, reset: boolean) { + const filterChanged = requested === -1; + const requestedPage = requested === -1 ? 0 : page; + + if (loading && !filterChanged) { + return; + } + + if (allProjectsFetched && !reset) { + const newUserId: number = userId === null ? -1 : userId; + + setProjects( + allProjects + .filter(project => + project.name.toUpperCase().includes(searchString.toUpperCase()) + ) + .filter( + project => + !ownProjects || + project.coaches.map(coach => coach.userId).includes(newUserId) + ) + ); + setMoreProjectsAvailable(false); + return; + } + setLoading(true); - const response = await getProjects(editionId, searchString, ownProjects, newPage); - setGotProjects(true); - if (response) { - if (response.projects.length === 0) { + if (controller !== undefined) { + controller.abort(); + } + const newController = new AbortController(); + setController(newController); + + const response = await toast.promise( + getProjects(editionId, searchString, ownProjects, requestedPage, newController), + { error: "Failed to retrieve projects" } + ); + + if (response !== null) { + if (response.projects.length === 0 && !filterChanged) { setMoreProjectsAvailable(false); } else { - setPage(page + 1); + setMoreProjectsAvailable(true); + } + if (requestedPage === 0 || filterChanged) { + setProjects(response.projects); + } else { setProjects(projects.concat(response.projects)); } + + if (searchString === "" && !ownProjects) { + if (response.projects.length === 0) { + setAllProjectsFetched(true); + } + if (requestedPage === 0) { + setAllProjects(response.projects); + } else { + setAllProjects(allProjects.concat(response.projects)); + } + } + + setPage(requestedPage + 1); + } else { + setMoreProjectsAvailable(false); } setLoading(false); } - async function refreshProjects() { - setProjects([]); - setPage(0); - setMoreProjectsAvailable(true); - setGotProjects(false); + useEffect(() => { + if (params.editionId !== requestedEdition) { + setProjects([]); + setPage(0); + setAllProjectsFetched(false); + setMoreProjectsAvailable(true); + loadProjects(-1, true); + setRequestedEdition(params.editionId); + } else { + setPage(0); + setMoreProjectsAvailable(true); + loadProjects(-1, false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchString, ownProjects, params.editionId]); + + /** + * Remove a project with a specific id + */ + function findAndRemoveProject(id: string, list: Project[]): Project[] { + return list.filter(project => project.projectId.toString() !== id); + } + + /** + * Find a project with a specific id and update its data + */ + function updateProject(project: Project, list: Project[]): Project[] { + const index = list.findIndex(pr => pr.projectId === project.projectId); + if (index === -1) return list; + + const copy = [...list]; + copy[index] = project; + + return copy; } + /** + * Websockets + */ useEffect(() => { - if (moreProjectsAvailable && !gotProjects) { - callProjects(0); + function listener(event: MessageEvent) { + const data = JSON.parse(event.data) as WebSocketEvent; + + // Ignore all events that aren't about projects + if (!wsEventTypes.includes(data.eventType)) return; + + // If the project from the event hasn't been loaded in the list, ignore the event as well + if ( + !allProjects.some( + project => project.projectId.toString() === data.pathIds.projectId + ) + ) { + return; + } + + // Project was deleted: remove it from the list + if (data.eventType === EventType.PROJECT && data.method === RequestMethod.DELETE) { + setAllProjects(findAndRemoveProject(data.pathIds.projectId!, allProjects)); + setProjects(findAndRemoveProject(data.pathIds.projectId!, projects)); + } else { + // Fetch the new version of the project & replace in the two lists + getProject(editionId, parseInt(data.pathIds.projectId!)).then(project => { + setAllProjects(updateProject(project!, allProjects)); + setProjects(updateProject(project!, projects)); + }); + } + } + + socket?.addEventListener("message", listener); + + function removeListener() { + if (socket) { + socket.removeEventListener("message", listener); + } } - }); + + return removeListener; + }, [socket, allProjects, projects, editionId]); return (
    -
    - setSearchString(e.target.value)} - placeholder="project name" - onKeyDown={e => { - if (e.key === "Enter") refreshProjects(); - }} - /> - Search - {role === Role.ADMIN && ( - navigate("/editions/" + editionId + "/projects/new")} - > - Create Project - - )} -
    + +
    + + { + setPage(0); + setSearchString(e.target.value); + }} + value={searchString} + placeholder="Search project..." + /> + + + {role === Role.ADMIN && !isReadonlyEdition(editionId, editions) && ( + navigate("/editions/" + editionId + "/projects/new")} + /> + )} +
    + +
    + { + setPage(0); setOwnProjects(!ownProjects); - refreshProjects(); }} /> - - { - console.log("loading more" + newPage); - }} - hasMore={moreProjectsAvailable} - useWindow={false} - initialLoad={true} - > - - - {projects.map((project, _index) => ( - - ))} - - - - - - - {moreProjectsAvailable && ( - - { - if (moreProjectsAvailable) { - callProjects(page); - } - }} - > - Load more projects - - - )} +
    ); } diff --git a/frontend/src/views/projectViews/ProjectsPage/styles.ts b/frontend/src/views/projectViews/ProjectsPage/styles.ts index 75f3b4804..6ecbbfe9b 100644 --- a/frontend/src/views/projectViews/ProjectsPage/styles.ts +++ b/frontend/src/views/projectViews/ProjectsPage/styles.ts @@ -8,50 +8,28 @@ export const CardsGrid = styled.div` grid-auto-flow: dense; `; -export const SearchField = styled.input` - margin: 20px 5px 5px 20px; - padding: 5px 10px; - background-color: #131329; - color: white; - border: none; - border-radius: 5px; +export const ControlContainer = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + margin: 20px; + margin-bottom: 10px; `; -export const SearchButton = styled.button` - padding: 5px 10px; - background-color: #00bfff; - color: white; - border: none; - border-radius: 5px; +export const SearchFieldDiv = styled.div` + margin-right: 10px; + float: left; + width: 15em; `; -export const CreateButton = styled.button` - margin-left: 25px; - padding: 5px 10px; - background-color: #44dba4; - color: white; - border: none; - border-radius: 5px; -`; export const OwnProject = styled(Form.Check)` margin-top: 10px; margin-left: 20px; + .form-check-input { + color: black; + } `; export const ProjectsContainer = styled.div` overflow: auto; `; - -export const LoadMoreContainer = styled.div` - display: flex; - justify-content: center; - margin: 20px; -`; - -export const LoadMoreButton = styled.button` - border-radius: 5px; - border: 0px; - padding: 5px 10px; - color: white; - background-color: gray; -`; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index b3d4746e3..53bbf1803 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4,14 +4,14 @@ "@ampproject/remapping@^2.1.0": version "2.1.2" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.1.2.tgz#4edca94973ded9630d20101cd8559cedb8d8bd34" + resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz" integrity sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg== dependencies: "@jridgewell/trace-mapping" "^0.3.0" "@apideck/better-ajv-errors@^0.3.1": version "0.3.3" - resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.3.tgz#ab0b1e981e1749bf59736cf7ebe25cfc9f949c15" + resolved "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.3.tgz" integrity sha512-9o+HO2MbJhJHjDYZaDxJmSDckvDpiuItEsrIShV0DXeCshXWRHhqYyU/PKHMkuClOmFnZhRd6wzv4vpDu/dRKg== dependencies: json-schema "^0.4.0" @@ -20,19 +20,24 @@ "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.8.3": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz" integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== dependencies: "@babel/highlight" "^7.16.7" "@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.8", "@babel/compat-data@^7.17.0", "@babel/compat-data@^7.17.7": version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.7.tgz#078d8b833fbbcc95286613be8c716cef2b519fa2" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.7.tgz" integrity sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ== +"@babel/compat-data@^7.17.10": + version "7.17.10" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.10.tgz#711dc726a492dfc8be8220028b1b92482362baab" + integrity sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw== + "@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.7.2", "@babel/core@^7.8.0": version "7.17.8" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.8.tgz#3dac27c190ebc3a4381110d46c80e77efe172e1a" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.17.8.tgz" integrity sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ== dependencies: "@ampproject/remapping" "^2.1.0" @@ -51,18 +56,48 @@ json5 "^2.1.2" semver "^6.3.0" +"@babel/core@^7.11.6": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.12.tgz#b4eb2d7ebc3449b062381644c93050db545b70ee" + integrity sha512-44ODe6O1IVz9s2oJE3rZ4trNNKTX9O7KpQpfAP4t8QII/zwrVRHL7i2pxhqtcY7tqMLrrKfMlBKnm1QlrRFs5w== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.12" + "@babel/helper-compilation-targets" "^7.17.10" + "@babel/helper-module-transforms" "^7.17.12" + "@babel/helpers" "^7.17.9" + "@babel/parser" "^7.17.12" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.12" + "@babel/types" "^7.17.12" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + "@babel/eslint-parser@^7.16.3": version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.17.0.tgz#eabb24ad9f0afa80e5849f8240d0e5facc2d90d6" + resolved "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.17.0.tgz" integrity sha512-PUEJ7ZBXbRkbq3qqM/jZ2nIuakUBqCYc7Qf52Lj7dlZ6zERnqisdHioL0l4wwQZnmskMeasqUNzLBFKs3nylXA== dependencies: eslint-scope "^5.1.1" eslint-visitor-keys "^2.1.0" semver "^6.3.0" +"@babel/generator@^7.17.12": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.12.tgz#5970e6160e9be0428e02f4aba62d8551ec366cc8" + integrity sha512-V49KtZiiiLjH/CnIW6OjJdrenrGoyh6AmKQ3k2AZFKozC1h846Q4NYlZ5nqAigPDUXfGzC88+LOUuG8yKd2kCw== + dependencies: + "@babel/types" "^7.17.12" + "@jridgewell/gen-mapping" "^0.3.0" + jsesc "^2.5.1" + "@babel/generator@^7.17.3", "@babel/generator@^7.17.7", "@babel/generator@^7.7.2": version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.7.tgz#8da2599beb4a86194a3b24df6c085931d9ee45ad" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz" integrity sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w== dependencies: "@babel/types" "^7.17.0" @@ -71,14 +106,14 @@ "@babel/helper-annotate-as-pure@^7.16.0", "@babel/helper-annotate-as-pure@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" + resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz" integrity sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw== dependencies: "@babel/types" "^7.16.7" "@babel/helper-builder-binary-assignment-operator-visitor@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz#38d138561ea207f0f69eb1626a418e4f7e6a580b" + resolved "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz" integrity sha512-C6FdbRaxYjwVu/geKW4ZeQ0Q31AftgRcdSnZ5/jsH6BzCJbtvXvhpfkbkThYSuutZA7nCXpPR6AD9zd1dprMkA== dependencies: "@babel/helper-explode-assignable-expression" "^7.16.7" @@ -86,7 +121,7 @@ "@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7", "@babel/helper-compilation-targets@^7.17.7": version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz#a3c2924f5e5f0379b356d4cfb313d1414dc30e46" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz" integrity sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w== dependencies: "@babel/compat-data" "^7.17.7" @@ -94,9 +129,19 @@ browserslist "^4.17.5" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.17.10": + version "7.17.10" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz#09c63106d47af93cf31803db6bc49fef354e2ebe" + integrity sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ== + dependencies: + "@babel/compat-data" "^7.17.10" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.20.2" + semver "^6.3.0" + "@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7", "@babel/helper-create-class-features-plugin@^7.17.6": version "7.17.6" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz#3778c1ed09a7f3e65e6d6e0f6fbfcc53809d92c9" + resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz" integrity sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg== dependencies: "@babel/helper-annotate-as-pure" "^7.16.7" @@ -109,7 +154,7 @@ "@babel/helper-create-regexp-features-plugin@^7.16.7": version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz#1dcc7d40ba0c6b6b25618997c5dbfd310f186fe1" + resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz" integrity sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA== dependencies: "@babel/helper-annotate-as-pure" "^7.16.7" @@ -117,7 +162,7 @@ "@babel/helper-define-polyfill-provider@^0.3.1": version "0.3.1" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz#52411b445bdb2e676869e5a74960d2d3826d2665" + resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz" integrity sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA== dependencies: "@babel/helper-compilation-targets" "^7.13.0" @@ -131,58 +176,66 @@ "@babel/helper-environment-visitor@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz#ff484094a839bde9d89cd63cba017d7aae80ecd7" + resolved "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz" integrity sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag== dependencies: "@babel/types" "^7.16.7" "@babel/helper-explode-assignable-expression@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz#12a6d8522fdd834f194e868af6354e8650242b7a" + resolved "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz" integrity sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ== dependencies: "@babel/types" "^7.16.7" "@babel/helper-function-name@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz#f1ec51551fb1c8956bc8dd95f38523b6cf375f8f" + resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz" integrity sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA== dependencies: "@babel/helper-get-function-arity" "^7.16.7" "@babel/template" "^7.16.7" "@babel/types" "^7.16.7" +"@babel/helper-function-name@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz#136fcd54bc1da82fcb47565cf16fd8e444b1ff12" + integrity sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg== + dependencies: + "@babel/template" "^7.16.7" + "@babel/types" "^7.17.0" + "@babel/helper-get-function-arity@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz#ea08ac753117a669f1508ba06ebcc49156387419" + resolved "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz" integrity sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw== dependencies: "@babel/types" "^7.16.7" "@babel/helper-hoist-variables@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" + resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz" integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== dependencies: "@babel/types" "^7.16.7" "@babel/helper-member-expression-to-functions@^7.16.7": version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz#a34013b57d8542a8c4ff8ba3f747c02452a4d8c4" + resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz" integrity sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw== dependencies: "@babel/types" "^7.17.0" "@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.0", "@babel/helper-module-imports@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz" integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== dependencies: "@babel/types" "^7.16.7" "@babel/helper-module-transforms@^7.16.7", "@babel/helper-module-transforms@^7.17.7": version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz#3943c7f777139e7954a5355c815263741a9c1cbd" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz" integrity sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw== dependencies: "@babel/helper-environment-visitor" "^7.16.7" @@ -194,21 +247,35 @@ "@babel/traverse" "^7.17.3" "@babel/types" "^7.17.0" +"@babel/helper-module-transforms@^7.17.12": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.12.tgz#bec00139520cb3feb078ef7a4578562480efb77e" + integrity sha512-t5s2BeSWIghhFRPh9XMn6EIGmvn8Lmw5RVASJzkIx1mSemubQQBNIZiQD7WzaFmaHIrjAec4x8z9Yx8SjJ1/LA== + dependencies: + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.12" + "@babel/types" "^7.17.12" + "@babel/helper-optimise-call-expression@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz#a34e3560605abbd31a18546bd2aad3e6d9a174f2" + resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz" integrity sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w== dependencies: "@babel/types" "^7.16.7" "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz" integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== "@babel/helper-remap-async-to-generator@^7.16.8": version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3" + resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz" integrity sha512-fm0gH7Flb8H51LqJHy3HJ3wnE1+qtYR2A99K06ahwrawLdOFsCEWjZOrYricXJHoPSudNKxrMBUPEIPxiIIvBw== dependencies: "@babel/helper-annotate-as-pure" "^7.16.7" @@ -217,7 +284,7 @@ "@babel/helper-replace-supers@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz#e9f5f5f32ac90429c1a4bdec0f231ef0c2838ab1" + resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz" integrity sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw== dependencies: "@babel/helper-environment-visitor" "^7.16.7" @@ -228,38 +295,38 @@ "@babel/helper-simple-access@^7.17.7": version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz#aaa473de92b7987c6dfa7ce9a7d9674724823367" + resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz" integrity sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA== dependencies: "@babel/types" "^7.17.0" "@babel/helper-skip-transparent-expression-wrappers@^7.16.0": version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz#0ee3388070147c3ae051e487eca3ebb0e2e8bb09" + resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz" integrity sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw== dependencies: "@babel/types" "^7.16.0" "@babel/helper-split-export-declaration@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" + resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz" integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== dependencies: "@babel/types" "^7.16.7" "@babel/helper-validator-identifier@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz" integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== "@babel/helper-validator-option@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz" integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== "@babel/helper-wrap-function@^7.16.8": version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz#58afda087c4cd235de92f7ceedebca2c41274200" + resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz" integrity sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw== dependencies: "@babel/helper-function-name" "^7.16.7" @@ -269,16 +336,25 @@ "@babel/helpers@^7.17.8": version "7.17.8" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.8.tgz#288450be8c6ac7e4e44df37bcc53d345e07bc106" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.8.tgz" integrity sha512-QcL86FGxpfSJwGtAvv4iG93UL6bmqBdmoVY0CMCU2g+oD2ezQse3PT5Pa+jiD6LJndBQi0EDlpzOWNlLuhz5gw== dependencies: "@babel/template" "^7.16.7" "@babel/traverse" "^7.17.3" "@babel/types" "^7.17.0" +"@babel/helpers@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.9.tgz#b2af120821bfbe44f9907b1826e168e819375a1a" + integrity sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.9" + "@babel/types" "^7.17.0" + "@babel/highlight@^7.16.7": version "7.16.10" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" + resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz" integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== dependencies: "@babel/helper-validator-identifier" "^7.16.7" @@ -287,19 +363,24 @@ "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7", "@babel/parser@^7.17.3", "@babel/parser@^7.17.8": version "7.17.8" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.8.tgz#2817fb9d885dd8132ea0f8eb615a6388cca1c240" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz" integrity sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ== +"@babel/parser@^7.17.12": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.12.tgz#36c2ed06944e3691ba82735fc4cf62d12d491a23" + integrity sha512-FLzHmN9V3AJIrWfOpvRlZCeVg/WLdicSnTMsLur6uDj9TT8ymUlG9XxURdW/XvuygK+2CW0poOJABdA4m/YKxA== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz#4eda6d6c2a0aa79c70fa7b6da67763dfe2141050" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz" integrity sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.7.tgz#cc001234dfc139ac45f6bcf801866198c8c72ff9" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.7.tgz" integrity sha512-di8vUHRdf+4aJ7ltXhaDbPoszdkh59AQtJM5soLsuHpQJdFQZOA4uGj0V2u/CZ8bJ/u8ULDL5yq6FO/bCXnKHw== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -308,7 +389,7 @@ "@babel/plugin-proposal-async-generator-functions@^7.16.8": version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz#3bdd1ebbe620804ea9416706cd67d60787504bc8" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz" integrity sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -317,7 +398,7 @@ "@babel/plugin-proposal-class-properties@^7.16.0", "@babel/plugin-proposal-class-properties@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz#925cad7b3b1a2fcea7e59ecc8eb5954f961f91b0" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz" integrity sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww== dependencies: "@babel/helper-create-class-features-plugin" "^7.16.7" @@ -325,7 +406,7 @@ "@babel/plugin-proposal-class-static-block@^7.16.7": version "7.17.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz#164e8fd25f0d80fa48c5a4d1438a6629325ad83c" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz" integrity sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA== dependencies: "@babel/helper-create-class-features-plugin" "^7.17.6" @@ -334,7 +415,7 @@ "@babel/plugin-proposal-decorators@^7.16.4": version "7.17.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.17.8.tgz#4f0444e896bee85d35cf714a006fc5418f87ff00" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.17.8.tgz" integrity sha512-U69odN4Umyyx1xO1rTII0IDkAEC+RNlcKXtqOblfpzqy1C+aOplb76BQNq0+XdpVkOaPlpEDwd++joY8FNFJKA== dependencies: "@babel/helper-create-class-features-plugin" "^7.17.6" @@ -345,7 +426,7 @@ "@babel/plugin-proposal-dynamic-import@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz#c19c897eaa46b27634a00fee9fb7d829158704b2" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz" integrity sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -353,7 +434,7 @@ "@babel/plugin-proposal-export-namespace-from@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz#09de09df18445a5786a305681423ae63507a6163" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz" integrity sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -361,7 +442,7 @@ "@babel/plugin-proposal-json-strings@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.7.tgz#9732cb1d17d9a2626a08c5be25186c195b6fa6e8" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.7.tgz" integrity sha512-lNZ3EEggsGY78JavgbHsK9u5P3pQaW7k4axlgFLYkMd7UBsiNahCITShLjNQschPyjtO6dADrL24757IdhBrsQ== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -369,7 +450,7 @@ "@babel/plugin-proposal-logical-assignment-operators@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz#be23c0ba74deec1922e639832904be0bea73cdea" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz" integrity sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -377,7 +458,7 @@ "@babel/plugin-proposal-nullish-coalescing-operator@^7.16.0", "@babel/plugin-proposal-nullish-coalescing-operator@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz#141fc20b6857e59459d430c850a0011e36561d99" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz" integrity sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -385,7 +466,7 @@ "@babel/plugin-proposal-numeric-separator@^7.16.0", "@babel/plugin-proposal-numeric-separator@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.7.tgz#d6b69f4af63fb38b6ca2558442a7fb191236eba9" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.7.tgz" integrity sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -393,7 +474,7 @@ "@babel/plugin-proposal-object-rest-spread@^7.16.7": version "7.17.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.17.3.tgz#d9eb649a54628a51701aef7e0ea3d17e2b9dd390" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.17.3.tgz" integrity sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw== dependencies: "@babel/compat-data" "^7.17.0" @@ -404,7 +485,7 @@ "@babel/plugin-proposal-optional-catch-binding@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.7.tgz#c623a430674ffc4ab732fd0a0ae7722b67cb74cf" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.7.tgz" integrity sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -412,7 +493,7 @@ "@babel/plugin-proposal-optional-chaining@^7.16.0", "@babel/plugin-proposal-optional-chaining@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz#7cd629564724816c0e8a969535551f943c64c39a" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz" integrity sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -421,7 +502,7 @@ "@babel/plugin-proposal-private-methods@^7.16.0", "@babel/plugin-proposal-private-methods@^7.16.11": version "7.16.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.11.tgz#e8df108288555ff259f4527dbe84813aac3a1c50" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.11.tgz" integrity sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw== dependencies: "@babel/helper-create-class-features-plugin" "^7.16.10" @@ -429,7 +510,7 @@ "@babel/plugin-proposal-private-property-in-object@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz#b0b8cef543c2c3d57e59e2c611994861d46a3fce" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz" integrity sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ== dependencies: "@babel/helper-annotate-as-pure" "^7.16.7" @@ -439,7 +520,7 @@ "@babel/plugin-proposal-unicode-property-regex@^7.16.7", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.7.tgz#635d18eb10c6214210ffc5ff4932552de08188a2" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.7.tgz" integrity sha512-QRK0YI/40VLhNVGIjRNAAQkEHws0cswSdFFjpFyt943YmJIU1da9uW63Iu6NFV6CxTZW5eTDCrwZUstBWgp/Rg== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.16.7" @@ -447,154 +528,154 @@ "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-bigint@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz" integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-class-properties@^7.12.13", "@babel/plugin-syntax-class-properties@^7.8.3": version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== dependencies: "@babel/helper-plugin-utils" "^7.12.13" "@babel/plugin-syntax-class-static-block@^7.14.5": version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz" integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-decorators@^7.17.0": version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.17.0.tgz#a2be3b2c9fe7d78bd4994e790896bc411e2f166d" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.17.0.tgz" integrity sha512-qWe85yCXsvDEluNP0OyeQjH63DlhAR3W7K9BxxU1MvbDb48tgBG+Ao6IJJ6smPDrrVzSQZrbF6donpkFBMcs3A== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-dynamic-import@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz" integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-export-namespace-from@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz" integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== dependencies: "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-flow@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.16.7.tgz#202b147e5892b8452bbb0bb269c7ed2539ab8832" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.16.7.tgz" integrity sha512-UDo3YGQO0jH6ytzVwgSLv9i/CzMcUjbKenL67dTrAZPPv6GFAtDhe6jqnvmoKzC/7htNTohhos+onPtDMqJwaQ== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-import-meta@^7.8.3": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz" integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.16.7": +"@babel/plugin-syntax-jsx@^7.12.13", "@babel/plugin-syntax-jsx@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz#50b6571d13f764266a113d77c82b4a6508bbe665" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz" integrity sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-optional-catch-binding@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-optional-chaining@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-private-property-in-object@^7.14.5": version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz" integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-top-level-await@^7.14.5", "@babel/plugin-syntax-top-level-await@^7.8.3": version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-typescript@^7.16.7", "@babel/plugin-syntax-typescript@^7.7.2": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz#39c9b55ee153151990fb038651d58d3fd03f98f8" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz" integrity sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-arrow-functions@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz#44125e653d94b98db76369de9c396dc14bef4154" + resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz" integrity sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-async-to-generator@^7.16.8": version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz#b83dff4b970cf41f1b819f8b49cc0cfbaa53a808" + resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz" integrity sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg== dependencies: "@babel/helper-module-imports" "^7.16.7" @@ -603,21 +684,21 @@ "@babel/plugin-transform-block-scoped-functions@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.7.tgz#4d0d57d9632ef6062cdf354bb717102ee042a620" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.7.tgz" integrity sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-block-scoping@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.7.tgz#f50664ab99ddeaee5bc681b8f3a6ea9d72ab4f87" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.7.tgz" integrity sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-classes@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz#8f4b9562850cd973de3b498f1218796eb181ce00" + resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz" integrity sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ== dependencies: "@babel/helper-annotate-as-pure" "^7.16.7" @@ -631,21 +712,21 @@ "@babel/plugin-transform-computed-properties@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.7.tgz#66dee12e46f61d2aae7a73710f591eb3df616470" + resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.7.tgz" integrity sha512-gN72G9bcmenVILj//sv1zLNaPyYcOzUho2lIJBMh/iakJ9ygCo/hEF9cpGb61SCMEDxbbyBoVQxrt+bWKu5KGw== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-destructuring@^7.16.7": version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.7.tgz#49dc2675a7afa9a5e4c6bdee636061136c3408d1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.7.tgz" integrity sha512-XVh0r5yq9sLR4vZ6eVZe8FKfIcSgaTBxVBRSYokRj2qksf6QerYnTxz9/GTuKTH/n/HwLP7t6gtlybHetJ/6hQ== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-dotall-regex@^7.16.7", "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.7.tgz#6b2d67686fab15fb6a7fd4bd895d5982cfc81241" + resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.7.tgz" integrity sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.16.7" @@ -653,14 +734,14 @@ "@babel/plugin-transform-duplicate-keys@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.7.tgz#2207e9ca8f82a0d36a5a67b6536e7ef8b08823c9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.7.tgz" integrity sha512-03DvpbRfvWIXyK0/6QiR1KMTWeT6OcQ7tbhjrXyFS02kjuX/mu5Bvnh5SDSWHxyawit2g5aWhKwI86EE7GUnTw== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-exponentiation-operator@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.7.tgz#efa9862ef97e9e9e5f653f6ddc7b665e8536fe9b" + resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.7.tgz" integrity sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA== dependencies: "@babel/helper-builder-binary-assignment-operator-visitor" "^7.16.7" @@ -668,7 +749,7 @@ "@babel/plugin-transform-flow-strip-types@^7.16.0": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.16.7.tgz#291fb140c78dabbf87f2427e7c7c332b126964b8" + resolved "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.16.7.tgz" integrity sha512-mzmCq3cNsDpZZu9FADYYyfZJIOrSONmHcop2XEKPdBNMa4PDC4eEvcOvzZaCNcjKu72v0XQlA5y1g58aLRXdYg== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -676,14 +757,14 @@ "@babel/plugin-transform-for-of@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.7.tgz#649d639d4617dff502a9a158c479b3b556728d8c" + resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.7.tgz" integrity sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-function-name@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.7.tgz#5ab34375c64d61d083d7d2f05c38d90b97ec65cf" + resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.7.tgz" integrity sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA== dependencies: "@babel/helper-compilation-targets" "^7.16.7" @@ -692,21 +773,21 @@ "@babel/plugin-transform-literals@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.7.tgz#254c9618c5ff749e87cb0c0cef1a0a050c0bdab1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.7.tgz" integrity sha512-6tH8RTpTWI0s2sV6uq3e/C9wPo4PTqqZps4uF0kzQ9/xPLFQtipynvmT1g/dOfEJ+0EQsHhkQ/zyRId8J2b8zQ== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-member-expression-literals@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.7.tgz#6e5dcf906ef8a098e630149d14c867dd28f92384" + resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.7.tgz" integrity sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-modules-amd@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.7.tgz#b28d323016a7daaae8609781d1f8c9da42b13186" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.7.tgz" integrity sha512-KaaEtgBL7FKYwjJ/teH63oAmE3lP34N3kshz8mm4VMAw7U3PxjVwwUmxEFksbgsNUaO3wId9R2AVQYSEGRa2+g== dependencies: "@babel/helper-module-transforms" "^7.16.7" @@ -715,7 +796,7 @@ "@babel/plugin-transform-modules-commonjs@^7.16.8": version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.7.tgz#d86b217c8e45bb5f2dbc11eefc8eab62cf980d19" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.7.tgz" integrity sha512-ITPmR2V7MqioMJyrxUo2onHNC3e+MvfFiFIR0RP21d3PtlVb6sfzoxNKiphSZUOM9hEIdzCcZe83ieX3yoqjUA== dependencies: "@babel/helper-module-transforms" "^7.17.7" @@ -725,7 +806,7 @@ "@babel/plugin-transform-modules-systemjs@^7.16.7": version "7.17.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.17.8.tgz#81fd834024fae14ea78fbe34168b042f38703859" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.17.8.tgz" integrity sha512-39reIkMTUVagzgA5x88zDYXPCMT6lcaRKs1+S9K6NKBPErbgO/w/kP8GlNQTC87b412ZTlmNgr3k2JrWgHH+Bw== dependencies: "@babel/helper-hoist-variables" "^7.16.7" @@ -736,7 +817,7 @@ "@babel/plugin-transform-modules-umd@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.7.tgz#23dad479fa585283dbd22215bff12719171e7618" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.7.tgz" integrity sha512-EMh7uolsC8O4xhudF2F6wedbSHm1HHZ0C6aJ7K67zcDNidMzVcxWdGr+htW9n21klm+bOn+Rx4CBsAntZd3rEQ== dependencies: "@babel/helper-module-transforms" "^7.16.7" @@ -744,21 +825,21 @@ "@babel/plugin-transform-named-capturing-groups-regex@^7.16.8": version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.8.tgz#7f860e0e40d844a02c9dcf9d84965e7dfd666252" + resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.8.tgz" integrity sha512-j3Jw+n5PvpmhRR+mrgIh04puSANCk/T/UA3m3P1MjJkhlK906+ApHhDIqBQDdOgL/r1UYpz4GNclTXxyZrYGSw== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.16.7" "@babel/plugin-transform-new-target@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.7.tgz#9967d89a5c243818e0800fdad89db22c5f514244" + resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.7.tgz" integrity sha512-xiLDzWNMfKoGOpc6t3U+etCE2yRnn3SM09BXqWPIZOBpL2gvVrBWUKnsJx0K/ADi5F5YC5f8APFfWrz25TdlGg== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-object-super@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz#ac359cf8d32cf4354d27a46867999490b6c32a94" + resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz" integrity sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -766,42 +847,42 @@ "@babel/plugin-transform-parameters@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.7.tgz#a1721f55b99b736511cb7e0152f61f17688f331f" + resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.7.tgz" integrity sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-property-literals@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.7.tgz#2dadac85155436f22c696c4827730e0fe1057a55" + resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.7.tgz" integrity sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-react-constant-elements@^7.12.1": version "7.17.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.17.6.tgz#6cc273c2f612a6a50cb657e63ee1303e5e68d10a" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.17.6.tgz" integrity sha512-OBv9VkyyKtsHZiHLoSfCn+h6yU7YKX8nrs32xUmOa1SRSk+t03FosB6fBZ0Yz4BpD1WV7l73Nsad+2Tz7APpqw== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-react-display-name@^7.16.0", "@babel/plugin-transform-react-display-name@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.16.7.tgz#7b6d40d232f4c0f550ea348593db3b21e2404340" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.16.7.tgz" integrity sha512-qgIg8BcZgd0G/Cz916D5+9kqX0c7nPZyXaP8R2tLNN5tkyIZdG5fEwBrxwplzSnjC1jvQmyMNVwUCZPcbGY7Pg== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-react-jsx-development@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz#43a00724a3ed2557ed3f276a01a929e6686ac7b8" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz" integrity sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A== dependencies: "@babel/plugin-transform-react-jsx" "^7.16.7" "@babel/plugin-transform-react-jsx@^7.16.7": version "7.17.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.3.tgz#eac1565da176ccb1a715dae0b4609858808008c1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.3.tgz" integrity sha512-9tjBm4O07f7mzKSIlEmPdiE6ub7kfIe6Cd+w+oQebpATfTQMAgW+YOuWxogbKVTulA+MEO7byMeIUtQ1z+z+ZQ== dependencies: "@babel/helper-annotate-as-pure" "^7.16.7" @@ -812,7 +893,7 @@ "@babel/plugin-transform-react-pure-annotations@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.16.7.tgz#232bfd2f12eb551d6d7d01d13fe3f86b45eb9c67" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.16.7.tgz" integrity sha512-hs71ToC97k3QWxswh2ElzMFABXHvGiJ01IB1TbYQDGeWRKWz/MPUTh5jGExdHvosYKpnJW5Pm3S4+TA3FyX+GA== dependencies: "@babel/helper-annotate-as-pure" "^7.16.7" @@ -820,21 +901,21 @@ "@babel/plugin-transform-regenerator@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.7.tgz#9e7576dc476cb89ccc5096fff7af659243b4adeb" + resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.7.tgz" integrity sha512-mF7jOgGYCkSJagJ6XCujSQg+6xC1M77/03K2oBmVJWoFGNUtnVJO4WHKJk3dnPC8HCcj4xBQP1Egm8DWh3Pb3Q== dependencies: regenerator-transform "^0.14.2" "@babel/plugin-transform-reserved-words@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.7.tgz#1d798e078f7c5958eec952059c460b220a63f586" + resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.7.tgz" integrity sha512-KQzzDnZ9hWQBjwi5lpY5v9shmm6IVG0U9pB18zvMu2i4H90xpT4gmqwPYsn8rObiadYe2M0gmgsiOIF5A/2rtg== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-runtime@^7.16.4": version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.0.tgz#0a2e08b5e2b2d95c4b1d3b3371a2180617455b70" + resolved "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.0.tgz" integrity sha512-fr7zPWnKXNc1xoHfrIU9mN/4XKX4VLZ45Q+oMhfsYIaHvg7mHgmhfOy/ckRWqDK7XF3QDigRpkh5DKq6+clE8A== dependencies: "@babel/helper-module-imports" "^7.16.7" @@ -846,14 +927,14 @@ "@babel/plugin-transform-shorthand-properties@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz#e8549ae4afcf8382f711794c0c7b6b934c5fbd2a" + resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz" integrity sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-spread@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.7.tgz#a303e2122f9f12e0105daeedd0f30fb197d8ff44" + resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.7.tgz" integrity sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -861,28 +942,28 @@ "@babel/plugin-transform-sticky-regex@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.7.tgz#c84741d4f4a38072b9a1e2e3fd56d359552e8660" + resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.7.tgz" integrity sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-template-literals@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.7.tgz#f3d1c45d28967c8e80f53666fc9c3e50618217ab" + resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.7.tgz" integrity sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-typeof-symbol@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.7.tgz#9cdbe622582c21368bd482b660ba87d5545d4f7e" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.7.tgz" integrity sha512-p2rOixCKRJzpg9JB4gjnG4gjWkWa89ZoYUnl9snJ1cWIcTH/hvxZqfO+WjG6T8DRBpctEol5jw1O5rA8gkCokQ== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-typescript@^7.16.7": version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.8.tgz#591ce9b6b83504903fa9dd3652c357c2ba7a1ee0" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.8.tgz" integrity sha512-bHdQ9k7YpBDO2d0NVfkj51DpQcvwIzIusJ7mEUaMlbZq3Kt/U47j24inXZHQ5MDiYpCs+oZiwnXyKedE8+q7AQ== dependencies: "@babel/helper-create-class-features-plugin" "^7.16.7" @@ -891,14 +972,14 @@ "@babel/plugin-transform-unicode-escapes@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz#da8717de7b3287a2c6d659750c964f302b31ece3" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz" integrity sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q== dependencies: "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-unicode-regex@^7.16.7": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.7.tgz#0f7aa4a501198976e25e82702574c34cfebe9ef2" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.7.tgz" integrity sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.16.7" @@ -906,7 +987,7 @@ "@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.16.4": version "7.16.11" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.16.11.tgz#5dd88fd885fae36f88fd7c8342475c9f0abe2982" + resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.16.11.tgz" integrity sha512-qcmWG8R7ZW6WBRPZK//y+E3Cli151B20W1Rv7ln27vuPaXU/8TKms6jFdiJtF7UDTxcrb7mZd88tAeK9LjdT8g== dependencies: "@babel/compat-data" "^7.16.8" @@ -986,7 +1067,7 @@ "@babel/preset-modules@^0.1.5": version "0.1.5" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" + resolved "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz" integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -997,7 +1078,7 @@ "@babel/preset-react@^7.12.5", "@babel/preset-react@^7.16.0": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.16.7.tgz#4c18150491edc69c183ff818f9f2aecbe5d93852" + resolved "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.16.7.tgz" integrity sha512-fWpyI8UM/HE6DfPBzD8LnhQ/OcH8AgTaqcqP2nGOXEUV+VKBR5JRN9hCk9ai+zQQ57vtm9oWeXguBCPNUjytgA== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -1009,7 +1090,7 @@ "@babel/preset-typescript@^7.16.0": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz#ab114d68bb2020afc069cd51b37ff98a046a70b9" + resolved "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz" integrity sha512-WbVEmgXdIyvzB77AQjGBEyYPZx+8tTsO50XtfozQrkW8QB2rLJpH2lgx0TRw5EJrBxOZQ+wCcyPVQvS8tjEHpQ== dependencies: "@babel/helper-plugin-utils" "^7.16.7" @@ -1018,22 +1099,22 @@ "@babel/runtime-corejs3@^7.10.2": version "7.17.8" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.17.8.tgz#d7dd49fb812f29c61c59126da3792d8740d4e284" + resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.17.8.tgz" integrity sha512-ZbYSUvoSF6dXZmMl/CYTMOvzIFnbGfv4W3SEHYgMvNsFTeLaF2gkGAF4K2ddmtSK4Emej+0aYcnSC6N5dPCXUQ== dependencies: core-js-pure "^3.20.2" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.16", "@babel/runtime@^7.13.8", "@babel/runtime@^7.14.6", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.17.8" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2" - integrity sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA== +"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.16", "@babel/runtime@^7.13.8", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72" + integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg== dependencies: regenerator-runtime "^0.13.4" "@babel/template@^7.16.7", "@babel/template@^7.3.3": version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz" integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== dependencies: "@babel/code-frame" "^7.16.7" @@ -1042,7 +1123,7 @@ "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.2": version "7.17.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.3.tgz#0ae0f15b27d9a92ba1f2263358ea7c4e7db47b57" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz" integrity sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw== dependencies: "@babel/code-frame" "^7.16.7" @@ -1056,27 +1137,51 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.17.12", "@babel/traverse@^7.17.9": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.12.tgz#011874d2abbca0ccf1adbe38f6f7a4ff1747599c" + integrity sha512-zULPs+TbCvOkIFd4FrG53xrpxvCBwLIgo6tO0tJorY7YV2IWFxUfS/lXDJbGgfyYt9ery/Gxj2niwttNnB0gIw== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.12" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.17.12" + "@babel/types" "^7.17.12" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.17.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz" integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw== dependencies: "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@babel/types@^7.17.12": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.12.tgz#1210690a516489c0200f355d87619157fbbd69a0" + integrity sha512-rH8i29wcZ6x9xjzI5ILHL/yZkbQnCERdHlogKuIb4PUr7do4iT8DPekrTbBLWTnRQm6U0GYABbTMSzijmEqlAg== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" - resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== "@csstools/normalize.css@*": version "12.0.0" - resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-12.0.0.tgz#a9583a75c3f150667771f30b60d9f059473e62c4" + resolved "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz" integrity sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg== "@csstools/postcss-color-function@^1.0.3": version "1.0.3" - resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-1.0.3.tgz#251c961a852c99e9aabdbbdbefd50e9a96e8a9ff" + resolved "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.0.3.tgz" integrity sha512-J26I69pT2B3MYiLY/uzCGKVJyMYVg9TCpXkWsRlt+Yfq+nELUEm72QXIMYXs4xA9cJA4Oqs2EylrfokKl3mJEQ== dependencies: "@csstools/postcss-progressive-custom-properties" "^1.1.0" @@ -1084,21 +1189,21 @@ "@csstools/postcss-font-format-keywords@^1.0.0": version "1.0.0" - resolved "https://registry.yarnpkg.com/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.0.tgz#7e7df948a83a0dfb7eb150a96e2390ac642356a1" + resolved "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.0.tgz" integrity sha512-oO0cZt8do8FdVBX8INftvIA4lUrKUSCcWUf9IwH9IPWOgKT22oAZFXeHLoDK7nhB2SmkNycp5brxfNMRLIhd6Q== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-hwb-function@^1.0.0": version "1.0.0" - resolved "https://registry.yarnpkg.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.0.tgz#d6785c1c5ba8152d1d392c66f3a6a446c6034f6d" + resolved "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.0.tgz" integrity sha512-VSTd7hGjmde4rTj1rR30sokY3ONJph1reCBTUXqeW1fKwETPy1x4t/XIeaaqbMbC5Xg4SM/lyXZ2S8NELT2TaA== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-ic-unit@^1.0.0": version "1.0.0" - resolved "https://registry.yarnpkg.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.0.tgz#f484db59fc94f35a21b6d680d23b0ec69b286b7f" + resolved "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.0.tgz" integrity sha512-i4yps1mBp2ijrx7E96RXrQXQQHm6F4ym1TOD0D69/sjDjZvQ22tqiEvaNw7pFZTUO5b9vWRHzbHzP9+UKuw+bA== dependencies: "@csstools/postcss-progressive-custom-properties" "^1.1.0" @@ -1106,21 +1211,21 @@ "@csstools/postcss-is-pseudo-class@^2.0.1": version "2.0.1" - resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.1.tgz#472fff2cf434bdf832f7145b2a5491587e790c9e" + resolved "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.1.tgz" integrity sha512-Og5RrTzwFhrKoA79c3MLkfrIBYmwuf/X83s+JQtz/Dkk/MpsaKtqHV1OOzYkogQ+tj3oYp5Mq39XotBXNqVc3Q== dependencies: postcss-selector-parser "^6.0.9" "@csstools/postcss-normalize-display-values@^1.0.0": version "1.0.0" - resolved "https://registry.yarnpkg.com/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.0.tgz#ce698f688c28517447aedf15a9037987e3d2dc97" + resolved "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.0.tgz" integrity sha512-bX+nx5V8XTJEmGtpWTO6kywdS725t71YSLlxWt78XoHUbELWgoCXeOFymRJmL3SU1TLlKSIi7v52EWqe60vJTQ== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-oklab-function@^1.0.2": version "1.0.2" - resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.0.2.tgz#87cd646e9450347a5721e405b4f7cc35157b7866" + resolved "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.0.2.tgz" integrity sha512-QwhWesEkMlp4narAwUi6pgc6kcooh8cC7zfxa9LSQNYXqzcdNUtNBzbGc5nuyAVreb7uf5Ox4qH1vYT3GA1wOg== dependencies: "@csstools/postcss-progressive-custom-properties" "^1.1.0" @@ -1128,82 +1233,150 @@ "@csstools/postcss-progressive-custom-properties@^1.1.0", "@csstools/postcss-progressive-custom-properties@^1.3.0": version "1.3.0" - resolved "https://registry.yarnpkg.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz#542292558384361776b45c85226b9a3a34f276fa" + resolved "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz" integrity sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA== dependencies: postcss-value-parser "^4.2.0" -"@emotion/is-prop-valid@^0.8.8": - version "0.8.8" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" - integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA== +"@emotion/babel-plugin@^11.7.1": + version "11.9.2" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.9.2.tgz#723b6d394c89fb2ef782229d92ba95a740576e95" + integrity sha512-Pr/7HGH6H6yKgnVFNEj2MVlreu3ADqftqjqwUvDy/OJzKFgxKeTQ+eeUf20FOTuHVkDON2iNa25rAXVYtWJCjw== + dependencies: + "@babel/helper-module-imports" "^7.12.13" + "@babel/plugin-syntax-jsx" "^7.12.13" + "@babel/runtime" "^7.13.10" + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.5" + "@emotion/serialize" "^1.0.2" + babel-plugin-macros "^2.6.1" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.0.13" + +"@emotion/cache@^11.4.0", "@emotion/cache@^11.7.1": + version "11.7.1" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539" + integrity sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A== + dependencies: + "@emotion/memoize" "^0.7.4" + "@emotion/sheet" "^1.1.0" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + stylis "4.0.13" + +"@emotion/hash@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + +"@emotion/is-prop-valid@^1.1.0": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.2.tgz#34ad6e98e871aa6f7a20469b602911b8b11b3a95" + integrity sha512-3QnhqeL+WW88YjYbQL5gUIkthuMw7a0NGbZ7wfFVk2kg/CK5w8w5FFa0RzWjyY1+sujN0NWbtSHH6OJmWHtJpQ== dependencies: - "@emotion/memoize" "0.7.4" + "@emotion/memoize" "^0.7.4" -"@emotion/memoize@0.7.4": - version "0.7.4" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" - integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== +"@emotion/memoize@^0.7.4", "@emotion/memoize@^0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" + integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== + +"@emotion/react@^11.8.1": + version "11.9.0" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.9.0.tgz#b6d42b1db3bd7511e7a7c4151dc8bc82e14593b8" + integrity sha512-lBVSF5d0ceKtfKCDQJveNAtkC7ayxpVlgOohLgXqRwqWr9bOf4TZAFFyIcNngnV6xK6X4x2ZeXq7vliHkoVkxQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@emotion/babel-plugin" "^11.7.1" + "@emotion/cache" "^11.7.1" + "@emotion/serialize" "^1.0.3" + "@emotion/utils" "^1.1.0" + "@emotion/weak-memoize" "^0.2.5" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.0.2", "@emotion/serialize@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.3.tgz#99e2060c26c6292469fb30db41f4690e1c8fea63" + integrity sha512-2mSSvgLfyV3q+iVh3YWgNlUc2a9ZlDU7DjuP5MjK3AXRR0dYigCrP99aeFtaB2L/hjfEZdSThn5dsZ0ufqbvsA== + dependencies: + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.4" + "@emotion/unitless" "^0.7.5" + "@emotion/utils" "^1.0.0" + csstype "^3.0.2" + +"@emotion/sheet@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2" + integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g== "@emotion/stylis@^0.8.4": version "0.8.5" - resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" + resolved "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz" integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== -"@emotion/unitless@^0.7.4": +"@emotion/unitless@^0.7.4", "@emotion/unitless@^0.7.5": version "0.7.5" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== -"@eslint/eslintrc@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.1.tgz#8b5e1c49f4077235516bc9ec7d41378c0f69b8c6" - integrity sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ== +"@emotion/utils@^1.0.0", "@emotion/utils@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.1.0.tgz#86b0b297f3f1a0f2bdb08eeac9a2f49afd40d0cf" + integrity sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ== + +"@emotion/weak-memoize@^0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" + integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== + +"@eslint/eslintrc@^1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.3.tgz#fcaa2bcef39e13d6e9e7f6271f4cc7cae1174886" + integrity sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.3.1" + espree "^9.3.2" globals "^13.9.0" ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" - minimatch "^3.0.4" + minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@fortawesome/fontawesome-common-types@6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.0.tgz#5a9468da0e5c2a3ccc161882ef5ffafbd3d4882f" - integrity sha512-lFIJ5opxOKG9q88xOsuJJAdRZ+2WRldsZwUR/7MJoOMUMhF/LkHUjwWACIEPTa5Wo6uTDHvGRIX+XutdN7zYxA== - -"@fortawesome/fontawesome-common-types@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz#949995a05c0d8801be7e0a594f775f1dbaa0d893" - integrity sha512-CA3MAZBTxVsF6SkfkHXDerkhcQs0QPofy43eFdbWJJkZiq3SfiaH1msOkac59rQaqto5EqWnASboY1dBuKen5w== +"@fortawesome/fontawesome-common-types@6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.1.tgz#7dc996042d21fc1ae850e3173b5c67b0549f9105" + integrity sha512-wVn5WJPirFTnzN6tR95abCx+ocH+3IFLXAgyavnf9hUmN0CfWoDjPT/BAWsUVwSlYYVBeCLJxaqi7ZGe4uSjBA== -"@fortawesome/fontawesome-svg-core@^1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.3.0.tgz#343fac91fa87daa630d26420bfedfba560f85885" - integrity sha512-UIL6crBWhjTNQcONt96ExjUnKt1D68foe3xjEensLDclqQ6YagwCRYVQdrp/hW0ALRp/5Fv/VKw+MqTUWYYvPg== +"@fortawesome/fontawesome-svg-core@^6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.1.tgz#3424ec6182515951816be9b11665d67efdce5b5f" + integrity sha512-NCg0w2YIp81f4V6cMGD9iomfsIj7GWrqmsa0ZsPh59G7PKiGN1KymZNxmF00ssuAlo/VZmpK6xazsGOwzKYUMg== dependencies: - "@fortawesome/fontawesome-common-types" "^0.3.0" + "@fortawesome/fontawesome-common-types" "6.1.1" -"@fortawesome/free-solid-svg-icons@^6.0.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.1.0.tgz#1bdc3ce6ddd2336348ba324ac4a72161725b0d95" - integrity sha512-OOr0jRHl5d41RzBS3sZh5Z3HmdPjMr43PxxKlYeLtQxFSixPf4sJFVM12/rTepB2m0rVShI0vtjHQmzOTlBaXg== +"@fortawesome/free-solid-svg-icons@^6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.1.1.tgz#3369e673f8fe8be2fba30b1ec274d47490a830a6" + integrity sha512-0/5exxavOhI/D4Ovm2r3vxNojGZioPwmFrKg0ZUH69Q68uFhFPs6+dhAToh6VEQBntxPRYPuT5Cg1tpNa9JUPg== dependencies: - "@fortawesome/fontawesome-common-types" "6.1.0" + "@fortawesome/fontawesome-common-types" "6.1.1" "@fortawesome/react-fontawesome@^0.1.17": version "0.1.18" - resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.18.tgz#dae37f718a24e14d7a99a5496c873d69af3fbd73" + resolved "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.18.tgz" integrity sha512-RwLIB4TZw0M9gvy5u+TusAA0afbwM4JQIimNH/j3ygd6aIvYPQLqXMhC9ErY26J23rDPyDZldIfPq/HpTTJ/tQ== dependencies: prop-types "^15.8.1" "@humanwhocodes/config-array@^0.9.2": version "0.9.5" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7" + resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz" integrity sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw== dependencies: "@humanwhocodes/object-schema" "^1.2.1" @@ -1212,12 +1385,12 @@ "@humanwhocodes/object-schema@^1.2.1": version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== dependencies: camelcase "^5.3.1" @@ -1228,12 +1401,12 @@ "@istanbuljs/schema@^0.1.2": version "0.1.3" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== "@jest/console@^27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.5.1.tgz#260fe7239602fe5130a94f1aa386eff54b014bba" + resolved "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz" integrity sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg== dependencies: "@jest/types" "^27.5.1" @@ -1243,9 +1416,21 @@ jest-util "^27.5.1" slash "^3.0.0" +"@jest/console@^28.1.0": + version "28.1.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-28.1.0.tgz#db78222c3d3b0c1db82f1b9de51094c2aaff2176" + integrity sha512-tscn3dlJFGay47kb4qVruQg/XWlmvU0xp3EJOjzzY+sBaI+YgwKcvAmTcyYU7xEiLLIY5HCdWRooAL8dqkFlDA== + dependencies: + "@jest/types" "^28.1.0" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^28.1.0" + jest-util "^28.1.0" + slash "^3.0.0" + "@jest/core@^27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.5.1.tgz#267ac5f704e09dc52de2922cbf3af9edcd64b626" + resolved "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz" integrity sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ== dependencies: "@jest/console" "^27.5.1" @@ -1277,9 +1462,44 @@ slash "^3.0.0" strip-ansi "^6.0.0" +"@jest/core@^28.1.0": + version "28.1.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-28.1.0.tgz#784a1e6ce5358b46fcbdcfbbd93b1b713ed4ea80" + integrity sha512-/2PTt0ywhjZ4NwNO4bUqD9IVJfmFVhVKGlhvSpmEfUCuxYf/3NHcKmRFI+I71lYzbTT3wMuYpETDCTHo81gC/g== + dependencies: + "@jest/console" "^28.1.0" + "@jest/reporters" "^28.1.0" + "@jest/test-result" "^28.1.0" + "@jest/transform" "^28.1.0" + "@jest/types" "^28.1.0" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^28.0.2" + jest-config "^28.1.0" + jest-haste-map "^28.1.0" + jest-message-util "^28.1.0" + jest-regex-util "^28.0.2" + jest-resolve "^28.1.0" + jest-resolve-dependencies "^28.1.0" + jest-runner "^28.1.0" + jest-runtime "^28.1.0" + jest-snapshot "^28.1.0" + jest-util "^28.1.0" + jest-validate "^28.1.0" + jest-watcher "^28.1.0" + micromatch "^4.0.4" + pretty-format "^28.1.0" + rimraf "^3.0.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + "@jest/environment@^27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.5.1.tgz#d7425820511fe7158abbecc010140c3fd3be9c74" + resolved "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz" integrity sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA== dependencies: "@jest/fake-timers" "^27.5.1" @@ -1287,9 +1507,34 @@ "@types/node" "*" jest-mock "^27.5.1" +"@jest/environment@^28.1.0": + version "28.1.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-28.1.0.tgz#dedf7d59ec341b9292fcf459fd0ed819eb2e228a" + integrity sha512-S44WGSxkRngzHslhV6RoAExekfF7Qhwa6R5+IYFa81mpcj0YgdBnRSmvHe3SNwOt64yXaE5GG8Y2xM28ii5ssA== + dependencies: + "@jest/fake-timers" "^28.1.0" + "@jest/types" "^28.1.0" + "@types/node" "*" + jest-mock "^28.1.0" + +"@jest/expect-utils@^28.1.0": + version "28.1.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-28.1.0.tgz#a5cde811195515a9809b96748ae8bcc331a3538a" + integrity sha512-5BrG48dpC0sB80wpeIX5FU6kolDJI4K0n5BM9a5V38MGx0pyRvUBSS0u2aNTdDzmOrCjhOg8pGs6a20ivYkdmw== + dependencies: + jest-get-type "^28.0.2" + +"@jest/expect@^28.1.0": + version "28.1.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-28.1.0.tgz#2e5a31db692597070932366a1602b5157f0f217c" + integrity sha512-be9ETznPLaHOmeJqzYNIXv1ADEzENuQonIoobzThOYPuK/6GhrWNIJDVTgBLCrz3Am73PyEU2urQClZp0hLTtA== + dependencies: + expect "^28.1.0" + jest-snapshot "^28.1.0" + "@jest/fake-timers@^27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74" + resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz" integrity sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ== dependencies: "@jest/types" "^27.5.1" @@ -1299,18 +1544,39 @@ jest-mock "^27.5.1" jest-util "^27.5.1" +"@jest/fake-timers@^28.1.0": + version "28.1.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-28.1.0.tgz#ea77878aabd5c5d50e1fc53e76d3226101e33064" + integrity sha512-Xqsf/6VLeAAq78+GNPzI7FZQRf5cCHj1qgQxCjws9n8rKw8r1UYoeaALwBvyuzOkpU3c1I6emeMySPa96rxtIg== + dependencies: + "@jest/types" "^28.1.0" + "@sinonjs/fake-timers" "^9.1.1" + "@types/node" "*" + jest-message-util "^28.1.0" + jest-mock "^28.1.0" + jest-util "^28.1.0" + "@jest/globals@^27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.5.1.tgz#7ac06ce57ab966566c7963431cef458434601b2b" + resolved "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz" integrity sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q== dependencies: "@jest/environment" "^27.5.1" "@jest/types" "^27.5.1" expect "^27.5.1" +"@jest/globals@^28.1.0": + version "28.1.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-28.1.0.tgz#a4427d2eb11763002ff58e24de56b84ba79eb793" + integrity sha512-3m7sTg52OTQR6dPhsEQSxAvU+LOBbMivZBwOvKEZ+Rb+GyxVnXi9HKgOTYkx/S99T8yvh17U4tNNJPIEQmtwYw== + dependencies: + "@jest/environment" "^28.1.0" + "@jest/expect" "^28.1.0" + "@jest/types" "^28.1.0" + "@jest/reporters@^27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.5.1.tgz#ceda7be96170b03c923c37987b64015812ffec04" + resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz" integrity sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw== dependencies: "@bcoe/v8-coverage" "^0.2.3" @@ -1339,18 +1605,64 @@ terminal-link "^2.0.0" v8-to-istanbul "^8.1.0" +"@jest/reporters@^28.1.0": + version "28.1.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-28.1.0.tgz#5183a28b9b593b6000fa9b89b031c7216b58a9a0" + integrity sha512-qxbFfqap/5QlSpIizH9c/bFCDKsQlM4uAKSOvZrP+nIdrjqre3FmKzpTtYyhsaVcOSNK7TTt2kjm+4BJIjysFA== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^28.1.0" + "@jest/test-result" "^28.1.0" + "@jest/transform" "^28.1.0" + "@jest/types" "^28.1.0" + "@jridgewell/trace-mapping" "^0.3.7" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^5.1.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-util "^28.1.0" + jest-worker "^28.1.0" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + terminal-link "^2.0.0" + v8-to-istanbul "^9.0.0" + +"@jest/schemas@^28.0.2": + version "28.0.2" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-28.0.2.tgz#08c30df6a8d07eafea0aef9fb222c5e26d72e613" + integrity sha512-YVDJZjd4izeTDkij00vHHAymNXQ6WWsdChFRK86qck6Jpr3DCL5W3Is3vslviRlP+bLuMYRLbdp98amMvqudhA== + dependencies: + "@sinclair/typebox" "^0.23.3" + "@jest/source-map@^27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.5.1.tgz#6608391e465add4205eae073b55e7f279e04e8cf" + resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz" integrity sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg== dependencies: callsites "^3.0.0" graceful-fs "^4.2.9" source-map "^0.6.0" +"@jest/source-map@^28.0.2": + version "28.0.2" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-28.0.2.tgz#914546f4410b67b1d42c262a1da7e0406b52dc90" + integrity sha512-Y9dxC8ZpN3kImkk0LkK5XCEneYMAXlZ8m5bflmSL5vrwyeUpJfentacCUg6fOb8NOpOO7hz2+l37MV77T6BFPw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.7" + callsites "^3.0.0" + graceful-fs "^4.2.9" + "@jest/test-result@^27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.5.1.tgz#56a6585fa80f7cdab72b8c5fc2e871d03832f5bb" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz" integrity sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag== dependencies: "@jest/console" "^27.5.1" @@ -1358,9 +1670,19 @@ "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" +"@jest/test-result@^28.1.0": + version "28.1.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-28.1.0.tgz#fd149dee123510dd2fcadbbf5f0020f98ad7f12c" + integrity sha512-sBBFIyoPzrZho3N+80P35A5oAkSKlGfsEFfXFWuPGBsW40UAjCkGakZhn4UQK4iQlW2vgCDMRDOob9FGKV8YoQ== + dependencies: + "@jest/console" "^28.1.0" + "@jest/types" "^28.1.0" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + "@jest/test-sequencer@^27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz#4057e0e9cea4439e544c6353c6affe58d095745b" + resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz" integrity sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ== dependencies: "@jest/test-result" "^27.5.1" @@ -1368,9 +1690,19 @@ jest-haste-map "^27.5.1" jest-runtime "^27.5.1" +"@jest/test-sequencer@^28.1.0": + version "28.1.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-28.1.0.tgz#ce7294bbe986415b9a30e218c7e705e6ebf2cdf2" + integrity sha512-tZCEiVWlWNTs/2iK9yi6o3AlMfbbYgV4uuZInSVdzZ7ftpHZhCMuhvk2HLYhCZzLgPFQ9MnM1YaxMnh3TILFiQ== + dependencies: + "@jest/test-result" "^28.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^28.1.0" + slash "^3.0.0" + "@jest/transform@^27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz" integrity sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw== dependencies: "@babel/core" "^7.1.0" @@ -1389,9 +1721,30 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" +"@jest/transform@^28.1.0": + version "28.1.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-28.1.0.tgz#224a3c9ba4cc98e2ff996c0a89a2d59db15c74ce" + integrity sha512-omy2xe5WxlAfqmsTjTPxw+iXRTRnf+NtX0ToG+4S0tABeb4KsKmPUHq5UBuwunHg3tJRwgEQhEp0M/8oiatLEA== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^28.1.0" + "@jridgewell/trace-mapping" "^0.3.7" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^28.1.0" + jest-regex-util "^28.0.2" + jest-util "^28.1.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.1" + "@jest/types@^27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" + resolved "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz" integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" @@ -1400,27 +1753,61 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" +"@jest/types@^28.1.0": + version "28.1.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-28.1.0.tgz#508327a89976cbf9bd3e1cc74641a29fd7dfd519" + integrity sha512-xmEggMPr317MIOjjDoZ4ejCSr9Lpbt/u34+dvc99t7DS8YirW5rwZEhzKPC2BMUFkUhI48qs6qLUSGw5FuL0GA== + dependencies: + "@jest/schemas" "^28.0.2" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz#cf92a983c83466b8c0ce9124fadeaf09f7c66ea9" + integrity sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/resolve-uri@^3.0.3": version "3.0.5" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz" integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew== +"@jridgewell/set-array@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.1.tgz#36a6acc93987adcf0ba50c66908bd0b70de8afea" + integrity sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ== + "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.11" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz" integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== "@jridgewell/trace-mapping@^0.3.0": version "0.3.4" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz" integrity sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ== dependencies: "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz#dcfe3e95f224c8fe97a87a5235defec999aa92ea" + integrity sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== dependencies: "@nodelib/fs.stat" "2.0.5" @@ -1428,12 +1815,12 @@ "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== "@nodelib/fs.walk@^1.2.3": version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== dependencies: "@nodelib/fs.scandir" "2.1.5" @@ -1441,7 +1828,7 @@ "@pmmmwh/react-refresh-webpack-plugin@^0.5.3": version "0.5.4" - resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.4.tgz#df0d0d855fc527db48aac93c218a0bf4ada41f99" + resolved "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.4.tgz" integrity sha512-zZbZeHQDnoTlt2AF+diQT0wsSXpvWiaIOZwBRdltNFhG1+I3ozyaw7U/nBiUwyJ0D+zwdXp0E3bWOl38Ag2BMw== dependencies: ansi-html-community "^0.0.8" @@ -1456,34 +1843,34 @@ "@popperjs/core@^2.10.1", "@popperjs/core@^2.10.2", "@popperjs/core@^2.8.6": version "2.11.4" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.4.tgz#d8c7b8db9226d2d7664553a0741ad7d0397ee503" + resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz" integrity sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg== "@react-aria/ssr@^3.0.1": version "3.1.2" - resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.1.2.tgz#665a6fd56385068c7417922af2d0d71b0618e52d" + resolved "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.1.2.tgz" integrity sha512-amXY11ImpokvkTMeKRHjsSsG7v1yzzs6yeqArCyBIk60J3Yhgxwx9Cah+Uu/804ATFwqzN22AXIo7SdtIaMP+g== dependencies: "@babel/runtime" "^7.6.2" "@restart/hooks@^0.3.26": version "0.3.27" - resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.3.27.tgz#91f356d66d4699a8cd8b3d008402708b6a9dc505" + resolved "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.27.tgz" integrity sha512-s984xV/EapUIfkjlf8wz9weP2O9TNKR96C68FfMEy2bE69+H4cNv3RD4Mf97lW7Htt7PjZrYTjSC8f3SB9VCXw== dependencies: dequal "^2.0.2" -"@restart/hooks@^0.4.0", "@restart/hooks@^0.4.5": - version "0.4.5" - resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.4.5.tgz#e7acbea237bfc9e479970500cf87538b41a1ed02" - integrity sha512-tLGtY0aHeIfT7aPwUkvQuhIy3+q3w4iqmUzFLPlOAf/vNUacLaBt1j/S//jv/dQhenRh8jvswyMojCwmLvJw8A== +"@restart/hooks@^0.4.0", "@restart/hooks@^0.4.6": + version "0.4.7" + resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.4.7.tgz#d79ca6472c01ce04389fc73d4a79af1b5e33cd39" + integrity sha512-ZbjlEHcG+FQtpDPHd7i4FzNNvJf2enAwZfJbpM8CW7BhmOAbsHpZe3tsHwfQUrBuyrxWqPYp2x5UMnilWcY22A== dependencies: dequal "^2.0.2" -"@restart/ui@^1.0.2": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@restart/ui/-/ui-1.1.0.tgz#46d436225162b47ecccdf191cfbcf9ec3d1d5f47" - integrity sha512-sYAO1LP78Suz5cT2VEkU4U/mvdjFXNg69QHanc5OAFTWyhCBG2lFJ9FITZ7hT8P8LPqcWXcwEGzHhuxPUDDDYQ== +"@restart/ui@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@restart/ui/-/ui-1.2.0.tgz#fb90251aa25f99b41ccedc78a91d2a15f3c5e0fb" + integrity sha512-oIh2t3tG8drZtZ9SlaV5CY6wGsUViHk8ZajjhcI+74IQHyWy+AnxDv8rJR5wVgsgcgrPBUvGNkC1AEdcGNPaLQ== dependencies: "@babel/runtime" "^7.13.16" "@popperjs/core" "^2.10.1" @@ -1492,13 +1879,12 @@ "@types/warning" "^3.0.0" dequal "^2.0.2" dom-helpers "^5.2.0" - prop-types "^15.7.2" uncontrollable "^7.2.1" warning "^4.0.3" "@rollup/plugin-babel@^5.2.0": version "5.3.1" - resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" + resolved "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz" integrity sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q== dependencies: "@babel/helper-module-imports" "^7.10.4" @@ -1506,7 +1892,7 @@ "@rollup/plugin-node-resolve@^11.2.1": version "11.2.1" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz#82aa59397a29cd4e13248b106e6a4a1880362a60" + resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz" integrity sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg== dependencies: "@rollup/pluginutils" "^3.1.0" @@ -1518,7 +1904,7 @@ "@rollup/plugin-replace@^2.4.1": version "2.4.2" - resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz#a2d539314fbc77c244858faa523012825068510a" + resolved "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz" integrity sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg== dependencies: "@rollup/pluginutils" "^3.1.0" @@ -1526,7 +1912,7 @@ "@rollup/pluginutils@^3.1.0": version "3.1.0" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" + resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz" integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== dependencies: "@types/estree" "0.0.39" @@ -1535,26 +1921,38 @@ "@rushstack/eslint-patch@^1.1.0": version "1.1.1" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.1.tgz#782fa5da44c4f38ae9fd38e9184b54e451936118" + resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.1.tgz" integrity sha512-BUyKJGdDWqvWC5GEhyOiUrGNi9iJUr4CU0O2WxJL6QJhHeeA/NVBalH+FeK0r/x/W0rPymXt5s78TDS7d6lCwg== +"@sinclair/typebox@^0.23.3": + version "0.23.5" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.23.5.tgz#93f7b9f4e3285a7a9ade7557d9a8d36809cbc47d" + integrity sha512-AFBVi/iT4g20DHoujvMH1aEDn8fGJh4xsRGCP6d8RpLPMqsNPvW01Jcn0QysXTsg++/xj25NmJsGyH9xug/wKg== + "@sinonjs/commons@^1.7.0": version "1.8.3" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" + resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz" integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== dependencies: type-detect "4.0.8" "@sinonjs/fake-timers@^8.0.1": version "8.1.0" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz#3fdc2b6cb58935b21bfb8d1625eb1300484316e7" + resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz" integrity sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg== dependencies: "@sinonjs/commons" "^1.7.0" +"@sinonjs/fake-timers@^9.1.1": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c" + integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw== + dependencies: + "@sinonjs/commons" "^1.7.0" + "@surma/rollup-plugin-off-main-thread@^2.2.3": version "2.2.3" - resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053" + resolved "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz" integrity sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ== dependencies: ejs "^3.1.6" @@ -1564,47 +1962,47 @@ "@svgr/babel-plugin-add-jsx-attribute@^5.4.0": version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz#81ef61947bb268eb9d50523446f9c638fb355906" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz" integrity sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg== "@svgr/babel-plugin-remove-jsx-attribute@^5.4.0": version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz#6b2c770c95c874654fd5e1d5ef475b78a0a962ef" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz" integrity sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg== "@svgr/babel-plugin-remove-jsx-empty-expression@^5.0.1": version "5.0.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz#25621a8915ed7ad70da6cea3d0a6dbc2ea933efd" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz" integrity sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA== "@svgr/babel-plugin-replace-jsx-attribute-value@^5.0.1": version "5.0.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz#0b221fc57f9fcd10e91fe219e2cd0dd03145a897" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz" integrity sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ== "@svgr/babel-plugin-svg-dynamic-title@^5.4.0": version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz#139b546dd0c3186b6e5db4fefc26cb0baea729d7" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz" integrity sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg== "@svgr/babel-plugin-svg-em-dimensions@^5.4.0": version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz#6543f69526632a133ce5cabab965deeaea2234a0" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz" integrity sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw== "@svgr/babel-plugin-transform-react-native-svg@^5.4.0": version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz#00bf9a7a73f1cad3948cdab1f8dfb774750f8c80" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz" integrity sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q== "@svgr/babel-plugin-transform-svg-component@^5.5.0": version "5.5.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz#583a5e2a193e214da2f3afeb0b9e8d3250126b4a" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz" integrity sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ== "@svgr/babel-preset@^5.5.0": version "5.5.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-5.5.0.tgz#8af54f3e0a8add7b1e2b0fcd5a882c55393df327" + resolved "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz" integrity sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig== dependencies: "@svgr/babel-plugin-add-jsx-attribute" "^5.4.0" @@ -1618,7 +2016,7 @@ "@svgr/core@^5.5.0": version "5.5.0" - resolved "https://registry.yarnpkg.com/@svgr/core/-/core-5.5.0.tgz#82e826b8715d71083120fe8f2492ec7d7874a579" + resolved "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz" integrity sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ== dependencies: "@svgr/plugin-jsx" "^5.5.0" @@ -1627,14 +2025,14 @@ "@svgr/hast-util-to-babel-ast@^5.5.0": version "5.5.0" - resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz#5ee52a9c2533f73e63f8f22b779f93cd432a5461" + resolved "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz" integrity sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ== dependencies: "@babel/types" "^7.12.6" "@svgr/plugin-jsx@^5.5.0": version "5.5.0" - resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz#1aa8cd798a1db7173ac043466d7b52236b369000" + resolved "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz" integrity sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA== dependencies: "@babel/core" "^7.12.3" @@ -1644,7 +2042,7 @@ "@svgr/plugin-svgo@^5.5.0": version "5.5.0" - resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz#02da55d85320549324e201c7b2e53bf431fcc246" + resolved "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz" integrity sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ== dependencies: cosmiconfig "^7.0.0" @@ -1653,7 +2051,7 @@ "@svgr/webpack@^5.5.0": version "5.5.0" - resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-5.5.0.tgz#aae858ee579f5fa8ce6c3166ef56c6a1b381b640" + resolved "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz" integrity sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g== dependencies: "@babel/core" "^7.12.3" @@ -1667,7 +2065,7 @@ "@testing-library/dom@^8.0.0": version "8.11.3" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.11.3.tgz#38fd63cbfe14557021e88982d931e33fb7c1a808" + resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.11.3.tgz" integrity sha512-9LId28I+lx70wUiZjLvi1DB/WT2zGOxUh46glrSNMaWVx849kKAluezVzZrXJfTKKoQTmEOutLes/bHg4Bj3aA== dependencies: "@babel/code-frame" "^7.10.4" @@ -1679,10 +2077,10 @@ lz-string "^1.4.4" pretty-format "^27.0.2" -"@testing-library/jest-dom@^5.14.1": - version "5.16.2" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.2.tgz#f329b36b44aa6149cd6ced9adf567f8b6aa1c959" - integrity sha512-6ewxs1MXWwsBFZXIk4nKKskWANelkdUehchEOokHsN8X7c2eKXGw+77aRV63UU8f/DTSVUPLaGxdrj4lN7D/ug== +"@testing-library/jest-dom@^5.16.4": + version "5.16.4" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz#938302d7b8b483963a3ae821f1c0808f872245cd" + integrity sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA== dependencies: "@babel/runtime" "^7.9.2" "@types/testing-library__jest-dom" "^5.9.1" @@ -1694,47 +2092,45 @@ lodash "^4.17.15" redent "^3.0.0" -"@testing-library/react@^12.0.0": - version "12.1.4" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.4.tgz#09674b117e550af713db3f4ec4c0942aa8bbf2c0" - integrity sha512-jiPKOm7vyUw311Hn/HlNQ9P8/lHNtArAx0PisXyFixDDvfl8DbD6EUdbshK5eqauvBSvzZd19itqQ9j3nferJA== +"@testing-library/react@^12.1.5": + version "12.1.5" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.5.tgz#bb248f72f02a5ac9d949dea07279095fa577963b" + integrity sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg== dependencies: "@babel/runtime" "^7.12.5" "@testing-library/dom" "^8.0.0" - "@types/react-dom" "*" + "@types/react-dom" "<18.0.0" -"@testing-library/user-event@^13.2.1": - version "13.5.0" - resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295" - integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg== - dependencies: - "@babel/runtime" "^7.12.5" +"@testing-library/user-event@^14.2.0": + version "14.2.0" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.2.0.tgz#8293560f8f80a00383d6c755ec3e0b918acb1683" + integrity sha512-+hIlG4nJS6ivZrKnOP7OGsDu9Fxmryj9vCl8x0ZINtTJcCHs2zLsYif5GzuRiBF2ck5GZG2aQr7Msg+EHlnYVQ== "@tootallnate/once@1": version "1.1.2" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== "@trysound/sax@0.2.0": version "0.2.0" - resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + resolved "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== "@types/aria-query@^4.2.0": version "4.2.2" - resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" + resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz" integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== "@types/axios@^0.14.0": version "0.14.0" - resolved "https://registry.yarnpkg.com/@types/axios/-/axios-0.14.0.tgz#ec2300fbe7d7dddd7eb9d3abf87999964cafce46" + resolved "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz" integrity sha1-7CMA++fX3d1+udOr+HmZlkyvzkY= dependencies: axios "*" "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": version "7.1.19" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz" integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== dependencies: "@babel/parser" "^7.1.0" @@ -1745,14 +2141,14 @@ "@types/babel__generator@*": version "7.6.4" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" + resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz" integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== dependencies: "@babel/types" "^7.0.0" "@types/babel__template@*": version "7.4.1" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" + resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz" integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== dependencies: "@babel/parser" "^7.1.0" @@ -1760,14 +2156,14 @@ "@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": version "7.14.2" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.14.2.tgz#ffcd470bbb3f8bf30481678fb5502278ca833a43" + resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.14.2.tgz" integrity sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA== dependencies: "@babel/types" "^7.3.0" "@types/body-parser@*": version "1.19.2" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz" integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== dependencies: "@types/connect" "*" @@ -1775,21 +2171,21 @@ "@types/bonjour@^3.5.9": version "3.5.10" - resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.10.tgz#0f6aadfe00ea414edc86f5d106357cda9701e275" + resolved "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz" integrity sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw== dependencies: "@types/node" "*" "@types/cheerio@*": version "0.22.31" - resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.31.tgz#b8538100653d6bb1b08a1e46dec75b4f2a5d5eb6" + resolved "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.31.tgz" integrity sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw== dependencies: "@types/node" "*" "@types/connect-history-api-fallback@^1.3.5": version "1.3.5" - resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" + resolved "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz" integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw== dependencies: "@types/express-serve-static-core" "*" @@ -1797,29 +2193,29 @@ "@types/connect@*": version "3.4.35" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz" integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== dependencies: "@types/node" "*" "@types/enzyme-adapter-react-16@^1.0.6": version "1.0.6" - resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.6.tgz#8aca7ae2fd6c7137d869b6616e696d21bb8b0cec" + resolved "https://registry.npmjs.org/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.6.tgz" integrity sha512-VonDkZ15jzqDWL8mPFIQnnLtjwebuL9YnDkqeCDYnB4IVgwUm0mwKkqhrxLL6mb05xm7qqa3IE95m8CZE9imCg== dependencies: "@types/enzyme" "*" -"@types/enzyme@*", "@types/enzyme@^3.10.11": - version "3.10.11" - resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.11.tgz#8924bd92cc63ac1843e215225dfa8f71555fe814" - integrity sha512-LEtC7zXsQlbGXWGcnnmOI7rTyP+i1QzQv4Va91RKXDEukLDaNyxu0rXlfMiGEhJwfgTPCTb0R+Pnlj//oM9e/w== +"@types/enzyme@*", "@types/enzyme@^3.10.12": + version "3.10.12" + resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.12.tgz#ac4494801b38188935580642f772ad18f72c132f" + integrity sha512-xryQlOEIe1TduDWAOphR0ihfebKFSWOXpIsk+70JskCfRfW+xALdnJ0r1ZOTo85F9Qsjk6vtlU7edTYHbls9tA== dependencies: "@types/cheerio" "*" "@types/react" "*" "@types/eslint-scope@^3.7.3": version "3.7.3" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224" + resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz" integrity sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g== dependencies: "@types/eslint" "*" @@ -1827,7 +2223,7 @@ "@types/eslint@*": version "8.4.1" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.1.tgz#c48251553e8759db9e656de3efc846954ac32304" + resolved "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz" integrity sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA== dependencies: "@types/estree" "*" @@ -1835,7 +2231,7 @@ "@types/eslint@^7.28.2": version "7.29.0" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.29.0.tgz#e56ddc8e542815272720bb0b4ccc2aff9c3e1c78" + resolved "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz" integrity sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng== dependencies: "@types/estree" "*" @@ -1843,17 +2239,17 @@ "@types/estree@*", "@types/estree@^0.0.51": version "0.0.51" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== "@types/estree@0.0.39": version "0.0.39" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": version "4.17.28" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz" integrity sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig== dependencies: "@types/node" "*" @@ -1862,7 +2258,7 @@ "@types/express@*", "@types/express@^4.17.13": version "4.17.13" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" + resolved "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz" integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== dependencies: "@types/body-parser" "*" @@ -1870,21 +2266,21 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/graceful-fs@^4.1.2": +"@types/graceful-fs@^4.1.2", "@types/graceful-fs@^4.1.3": version "4.1.5" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" + resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz" integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== dependencies: "@types/node" "*" "@types/history@^4.7.11": version "4.7.11" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" + resolved "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz" integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== -"@types/hoist-non-react-statics@*": +"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0": version "3.3.1" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz" integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== dependencies: "@types/react" "*" @@ -1892,43 +2288,38 @@ "@types/html-minifier-terser@^6.0.0": version "6.1.0" - resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" + resolved "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz" integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== "@types/http-proxy@^1.17.8": version "1.17.8" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.8.tgz#968c66903e7e42b483608030ee85800f22d03f55" + resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz" integrity sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA== dependencies: "@types/node" "*" -"@types/invariant@^2.2.35": - version "2.2.35" - resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.35.tgz#cd3ebf581a6557452735688d8daba6cf0bd5a3be" - integrity sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg== - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz" integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== "@types/istanbul-lib-report@*": version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + resolved "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== dependencies: "@types/istanbul-lib-coverage" "*" "@types/istanbul-reports@^3.0.0": version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz" integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== dependencies: "@types/istanbul-lib-report" "*" "@types/jest@*", "@types/jest@^27.0.1": version "27.4.1" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d" + resolved "https://registry.npmjs.org/@types/jest/-/jest-27.4.1.tgz" integrity sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw== dependencies: jest-matcher-utils "^27.0.0" @@ -1936,76 +2327,88 @@ "@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.10" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.10.tgz#9b05b7896166cd00e9cbd59864853abf65d9ac23" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.10.tgz" integrity sha512-BLO9bBq59vW3fxCpD4o0N4U+DXsvwvIcl+jofw0frQo/GrBFC+/jRZj1E7kgp6dvTyNmA4y6JCV5Id/r3mNP5A== "@types/json5@^0.0.29": version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= "@types/mime@^1": version "1.3.2" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== -"@types/node@*": - version "17.0.21" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644" - integrity sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ== - -"@types/node@^16.7.13": - version "16.11.26" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.26.tgz#63d204d136c9916fb4dcd1b50f9740fe86884e47" - integrity sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ== +"@types/node@*", "@types/node@^17.0.34": + version "17.0.34" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.34.tgz#3b0b6a50ff797280b8d000c6281d229f9c538cef" + integrity sha512-XImEz7XwTvDBtzlTnm8YvMqGW/ErMWBsKZ+hMTvnDIjGCKxwK5Xpc+c/oQjOauwq8M4OS11hEkpjX8rrI/eEgA== "@types/parse-json@^4.0.0": version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== "@types/prettier@^2.1.5": version "2.4.4" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.4.tgz#5d9b63132df54d8909fce1c3f8ca260fdd693e17" + resolved "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.4.tgz" integrity sha512-ReVR2rLTV1kvtlWFyuot+d1pkpG2Fw/XKE3PDAdj57rbM97ttSp9JZ2UsP+2EHTylra9cUf6JA7tGwW1INzUrA== -"@types/prop-types@*", "@types/prop-types@^15.7.4": +"@types/prop-types@*": version "15.7.4" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" + resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz" integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== "@types/q@^1.5.1": version "1.5.5" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df" + resolved "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz" integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ== "@types/qs@*": version "6.9.7" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz" integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== "@types/range-parser@*": version "1.2.4" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/react-dom@*", "@types/react-dom@^17.0.9": - version "17.0.14" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.14.tgz#c8f917156b652ddf807711f5becbd2ab018dea9f" - integrity sha512-H03xwEP1oXmSfl3iobtmQ/2dHF5aBHr8aUMwyGZya6OW45G+xtdzmq6HkncefiBt5JU8DVyaWl/nWZbjZCnzAQ== +"@types/react-beautiful-dnd@^13.1.2": + version "13.1.2" + resolved "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz" + integrity sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg== dependencies: "@types/react" "*" +"@types/react-dom@<18.0.0", "@types/react-dom@^17.0.9": + version "17.0.17" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.17.tgz#2e3743277a793a96a99f1bf87614598289da68a1" + integrity sha512-VjnqEmqGnasQKV0CWLevqMTXBYG9GbwuE6x3VetERLh0cq2LTptFE73MrQi2S7GkKXCf2GgwItB/melLnxfnsg== + dependencies: + "@types/react" "^17" + "@types/react-infinite-scroller@^1.2.3": version "1.2.3" - resolved "https://registry.yarnpkg.com/@types/react-infinite-scroller/-/react-infinite-scroller-1.2.3.tgz#b8dcb0e5762c3f79cc92e574d2c77402524cab71" + resolved "https://registry.npmjs.org/@types/react-infinite-scroller/-/react-infinite-scroller-1.2.3.tgz" integrity sha512-l60JckVoO+dxmKW2eEG7jbliEpITsTJvRPTe97GazjF5+ylagAuyYdXl8YY9DQsTP9QjhqGKZROknzgscGJy0A== dependencies: "@types/react" "*" +"@types/react-redux@^7.1.20": + version "7.1.24" + resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz" + integrity sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + "@types/react-router-bootstrap@^0.24.5": version "0.24.5" - resolved "https://registry.yarnpkg.com/@types/react-router-bootstrap/-/react-router-bootstrap-0.24.5.tgz#9257ba3dfb01cda201aac9fa05cde3eb09ea5b27" + resolved "https://registry.npmjs.org/@types/react-router-bootstrap/-/react-router-bootstrap-0.24.5.tgz" integrity sha512-GRx/8xF/skw4/Pmm6d+xbExi8gobCLOe8Eoz9kXPQGbYo7p5Wbi61tjpOF5AbfJ5XMN+fIzweToTi56odj/LOQ== dependencies: "@types/react" "*" @@ -2013,7 +2416,7 @@ "@types/react-router-dom@*", "@types/react-router-dom@^5.3.3": version "5.3.3" - resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" + resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz" integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== dependencies: "@types/history" "^4.7.11" @@ -2022,23 +2425,23 @@ "@types/react-router@*": version "5.1.18" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.18.tgz#c8851884b60bc23733500d86c1266e1cfbbd9ef3" + resolved "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.18.tgz" integrity sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g== dependencies: "@types/history" "^4.7.11" "@types/react" "*" -"@types/react-transition-group@^4.4.4": +"@types/react-transition-group@^4.4.0", "@types/react-transition-group@^4.4.4": version "4.4.4" - resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e" + resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz" integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug== dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16.14.8", "@types/react@>=16.9.11", "@types/react@^17.0.20": - version "17.0.41" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.41.tgz#6e179590d276394de1e357b3f89d05d7d3da8b85" - integrity sha512-chYZ9ogWUodyC7VUTRBfblysKLjnohhFY9bGLwvnUFFy48+vB9DikmB3lW0qTFmBcKSzmdglcvkHK71IioOlDA== +"@types/react@*", "@types/react@>=16.9.11", "@types/react@^17", "@types/react@^17.0.20": + version "17.0.45" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.45.tgz#9b3d5b661fd26365fefef0e766a1c6c30ccf7b3f" + integrity sha512-YfhQ22Lah2e3CHPsb93tRwIGNiSwkuz1/blk4e6QrWS0jQzCSNbGLtOEYhPg02W0yGTTmpajp7dCTbBAMN3qsg== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -2046,31 +2449,31 @@ "@types/resolve@1.17.1": version "1.17.1" - resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" + resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz" integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== dependencies: "@types/node" "*" "@types/retry@^0.12.0": version "0.12.1" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065" + resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz" integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g== "@types/scheduler@*": version "0.16.2" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== "@types/serve-index@^1.9.1": version "1.9.1" - resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278" + resolved "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz" integrity sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg== dependencies: "@types/express" "*" "@types/serve-static@*": version "1.13.10" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" + resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz" integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ== dependencies: "@types/mime" "^1" @@ -2078,20 +2481,20 @@ "@types/sockjs@^0.3.33": version "0.3.33" - resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" + resolved "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz" integrity sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw== dependencies: "@types/node" "*" "@types/stack-utils@^2.0.0": version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" + resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/styled-components@^5.1.24": - version "5.1.24" - resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.24.tgz#b52ae677f03ea8a6018aa34c6c96b7018b7a3571" - integrity sha512-mz0fzq2nez+Lq5IuYammYwWgyLUE6OMAJTQL9D8hFLP4Pkh7gVYJii/VQWxq8/TK34g/OrkehXaFNdcEKcItug== +"@types/styled-components@^5.1.25": + version "5.1.25" + resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.25.tgz#0177c4ab5fa7c6ed0565d36f597393dae3f380ad" + integrity sha512-fgwl+0Pa8pdkwXRoVPP9JbqF0Ivo9llnmsm+7TCI330kbPIFd9qv1Lrhr37shf4tnxCOSu+/IgqM7uJXLWZZNQ== dependencies: "@types/hoist-non-react-statics" "*" "@types/react" "*" @@ -2099,97 +2502,117 @@ "@types/testing-library__jest-dom@^5.9.1": version "5.14.3" - resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.3.tgz#ee6c7ffe9f8595882ee7bda8af33ae7b8789ef17" + resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.3.tgz" integrity sha512-oKZe+Mf4ioWlMuzVBaXQ9WDnEm1+umLx0InILg+yvZVBBDmzV5KfZyLrCvadtWcx8+916jLmHafcmqqffl+iIw== dependencies: "@types/jest" "*" "@types/trusted-types@^2.0.2": version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" + resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz" integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== "@types/warning@^3.0.0": version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52" + resolved "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz" integrity sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI= "@types/ws@^8.2.2": version "8.5.3" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" + resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz" integrity sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w== dependencies: "@types/node" "*" "@types/yargs-parser@*": version "21.0.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz" integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== "@types/yargs@^16.0.0": version "16.0.4" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz" integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.12.0", "@typescript-eslint/eslint-plugin@^5.5.0": - version "5.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.15.0.tgz#c28ef7f2e688066db0b6a9d95fb74185c114fb9a" - integrity sha512-u6Db5JfF0Esn3tiAKELvoU5TpXVSkOpZ78cEGn/wXtT2RVqs2vkt4ge6N8cRCyw7YVKhmmLDbwI2pg92mlv7cA== +"@types/yargs@^17.0.8": + version "17.0.10" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.10.tgz#591522fce85d8739bca7b8bb90d048e4478d186a" + integrity sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA== dependencies: - "@typescript-eslint/scope-manager" "5.15.0" - "@typescript-eslint/type-utils" "5.15.0" - "@typescript-eslint/utils" "5.15.0" - debug "^4.3.2" + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@^5.25.0", "@typescript-eslint/eslint-plugin@^5.5.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.25.0.tgz#e8ce050990e4d36cc200f2de71ca0d3eb5e77a31" + integrity sha512-icYrFnUzvm+LhW0QeJNKkezBu6tJs9p/53dpPLFH8zoM9w1tfaKzVurkPotEpAqQ8Vf8uaFyL5jHd0Vs6Z0ZQg== + dependencies: + "@typescript-eslint/scope-manager" "5.25.0" + "@typescript-eslint/type-utils" "5.25.0" + "@typescript-eslint/utils" "5.25.0" + debug "^4.3.4" functional-red-black-tree "^1.0.1" - ignore "^5.1.8" + ignore "^5.2.0" regexpp "^3.2.0" - semver "^7.3.5" + semver "^7.3.7" tsutils "^3.21.0" "@typescript-eslint/experimental-utils@^5.0.0": version "5.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.15.0.tgz#407bbbdf1d11d24de81cfdf556b3a9f4252ba4ae" + resolved "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.15.0.tgz" integrity sha512-AJOOaBrVqKYWaYDBtgMi9XVDB3YHXlffto/3A4VQ39VVaNqosSOp/nW09G4N/ej8WlzHQB2jTnSfP5wWsXSQJA== dependencies: "@typescript-eslint/utils" "5.15.0" -"@typescript-eslint/parser@^5.12.0", "@typescript-eslint/parser@^5.5.0": - version "5.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.15.0.tgz#95f603f8fe6eca7952a99bfeef9b85992972e728" - integrity sha512-NGAYP/+RDM2sVfmKiKOCgJYPstAO40vPAgACoWPO/+yoYKSgAXIFaBKsV8P0Cc7fwKgvj27SjRNX4L7f4/jCKQ== +"@typescript-eslint/parser@^5.25.0", "@typescript-eslint/parser@^5.5.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.25.0.tgz#fb533487147b4b9efd999a4d2da0b6c263b64f7f" + integrity sha512-r3hwrOWYbNKP1nTcIw/aZoH+8bBnh/Lh1iDHoFpyG4DnCpvEdctrSl6LOo19fZbzypjQMHdajolxs6VpYoChgA== dependencies: - "@typescript-eslint/scope-manager" "5.15.0" - "@typescript-eslint/types" "5.15.0" - "@typescript-eslint/typescript-estree" "5.15.0" - debug "^4.3.2" + "@typescript-eslint/scope-manager" "5.25.0" + "@typescript-eslint/types" "5.25.0" + "@typescript-eslint/typescript-estree" "5.25.0" + debug "^4.3.4" "@typescript-eslint/scope-manager@5.15.0": version "5.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.15.0.tgz#d97afab5e0abf4018d1289bd711be21676cdd0ee" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.15.0.tgz" integrity sha512-EFiZcSKrHh4kWk0pZaa+YNJosvKE50EnmN4IfgjkA3bTHElPtYcd2U37QQkNTqwMCS7LXeDeZzEqnsOH8chjSg== dependencies: "@typescript-eslint/types" "5.15.0" "@typescript-eslint/visitor-keys" "5.15.0" -"@typescript-eslint/type-utils@5.15.0": - version "5.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.15.0.tgz#d2c02eb2bdf54d0a645ba3a173ceda78346cf248" - integrity sha512-KGeDoEQ7gHieLydujGEFLyLofipe9PIzfvA/41urz4hv+xVxPEbmMQonKSynZ0Ks2xDhJQ4VYjB3DnRiywvKDA== +"@typescript-eslint/scope-manager@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.25.0.tgz#e78f1484bca7e484c48782075219c82c6b77a09f" + integrity sha512-p4SKTFWj+2VpreUZ5xMQsBMDdQ9XdRvODKXN4EksyBjFp2YvQdLkyHqOffakYZPuWJUDNu3jVXtHALDyTv3cww== dependencies: - "@typescript-eslint/utils" "5.15.0" - debug "^4.3.2" + "@typescript-eslint/types" "5.25.0" + "@typescript-eslint/visitor-keys" "5.25.0" + +"@typescript-eslint/type-utils@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.25.0.tgz#5750d26a5db4c4d68d511611e0ada04e56f613bc" + integrity sha512-B6nb3GK3Gv1Rsb2pqalebe/RyQoyG/WDy9yhj8EE0Ikds4Xa8RR28nHz+wlt4tMZk5bnAr0f3oC8TuDAd5CPrw== + dependencies: + "@typescript-eslint/utils" "5.25.0" + debug "^4.3.4" tsutils "^3.21.0" "@typescript-eslint/types@5.15.0": version "5.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.15.0.tgz#c7bdd103843b1abae97b5518219d3e2a0d79a501" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.15.0.tgz" integrity sha512-yEiTN4MDy23vvsIksrShjNwQl2vl6kJeG9YkVJXjXZnkJElzVK8nfPsWKYxcsGWG8GhurYXP4/KGj3aZAxbeOA== +"@typescript-eslint/types@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.25.0.tgz#dee51b1855788b24a2eceeae54e4adb89b088dd8" + integrity sha512-7fWqfxr0KNHj75PFqlGX24gWjdV/FDBABXL5dyvBOWHpACGyveok8Uj4ipPX/1fGU63fBkzSIycEje4XsOxUFA== + "@typescript-eslint/typescript-estree@5.15.0": version "5.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.15.0.tgz#81513a742a9c657587ad1ddbca88e76c6efb0aac" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.15.0.tgz" integrity sha512-Hb0e3dGc35b75xLzixM3cSbG1sSbrTBQDfIScqdyvrfJZVEi4XWAT+UL/HMxEdrJNB8Yk28SKxPLtAhfCbBInA== dependencies: "@typescript-eslint/types" "5.15.0" @@ -2200,9 +2623,22 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/utils@5.15.0", "@typescript-eslint/utils@^5.13.0": +"@typescript-eslint/typescript-estree@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.25.0.tgz#a7ab40d32eb944e3fb5b4e3646e81b1bcdd63e00" + integrity sha512-MrPODKDych/oWs/71LCnuO7NyR681HuBly2uLnX3r5i4ME7q/yBqC4hW33kmxtuauLTM0OuBOhhkFaxCCOjEEw== + dependencies: + "@typescript-eslint/types" "5.25.0" + "@typescript-eslint/visitor-keys" "5.25.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.15.0": version "5.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.15.0.tgz#468510a0974d3ced8342f37e6c662778c277f136" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.15.0.tgz" integrity sha512-081rWu2IPKOgTOhHUk/QfxuFog8m4wxW43sXNOMSCdh578tGJ1PAaWPsj42LOa7pguh173tNlMigsbrHvh/mtA== dependencies: "@types/json-schema" "^7.0.9" @@ -2212,17 +2648,37 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" +"@typescript-eslint/utils@5.25.0", "@typescript-eslint/utils@^5.13.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.25.0.tgz#272751fd737733294b4ab95e16c7f2d4a75c2049" + integrity sha512-qNC9bhnz/n9Kba3yI6HQgQdBLuxDoMgdjzdhSInZh6NaDnFpTUlwNGxplUFWfY260Ya0TRPvkg9dd57qxrJI9g== + dependencies: + "@types/json-schema" "^7.0.9" + "@typescript-eslint/scope-manager" "5.25.0" + "@typescript-eslint/types" "5.25.0" + "@typescript-eslint/typescript-estree" "5.25.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + "@typescript-eslint/visitor-keys@5.15.0": version "5.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.15.0.tgz#5669739fbf516df060f978be6a6dce75855a8027" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.15.0.tgz" integrity sha512-+vX5FKtgvyHbmIJdxMJ2jKm9z2BIlXJiuewI8dsDYMp5LzPUcuTT78Ya5iwvQg3VqSVdmxyM8Anj1Jeq7733ZQ== dependencies: "@typescript-eslint/types" "5.15.0" eslint-visitor-keys "^3.0.0" +"@typescript-eslint/visitor-keys@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.25.0.tgz#33aa5fdcc5cedb9f4c8828c6a019d58548d4474b" + integrity sha512-yd26vFgMsC4h2dgX4+LR+GeicSKIfUvZREFLf3DDjZPtqgLx5AJZr6TetMNwFP9hcKreTTeztQYBTNbNoOycwA== + dependencies: + "@typescript-eslint/types" "5.25.0" + eslint-visitor-keys "^3.3.0" + "@webassemblyjs/ast@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" + resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz" integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== dependencies: "@webassemblyjs/helper-numbers" "1.11.1" @@ -2230,22 +2686,22 @@ "@webassemblyjs/floating-point-hex-parser@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" + resolved "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz" integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== "@webassemblyjs/helper-api-error@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz" integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== "@webassemblyjs/helper-buffer@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz" integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== "@webassemblyjs/helper-numbers@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz" integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== dependencies: "@webassemblyjs/floating-point-hex-parser" "1.11.1" @@ -2254,12 +2710,12 @@ "@webassemblyjs/helper-wasm-bytecode@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz" integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== "@webassemblyjs/helper-wasm-section@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz" integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== dependencies: "@webassemblyjs/ast" "1.11.1" @@ -2269,26 +2725,26 @@ "@webassemblyjs/ieee754@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" + resolved "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz" integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== dependencies: "@xtuc/ieee754" "^1.2.0" "@webassemblyjs/leb128@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" + resolved "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz" integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== dependencies: "@xtuc/long" "4.2.2" "@webassemblyjs/utf8@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" + resolved "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz" integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== "@webassemblyjs/wasm-edit@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz" integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== dependencies: "@webassemblyjs/ast" "1.11.1" @@ -2302,7 +2758,7 @@ "@webassemblyjs/wasm-gen@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz" integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== dependencies: "@webassemblyjs/ast" "1.11.1" @@ -2313,7 +2769,7 @@ "@webassemblyjs/wasm-opt@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz" integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== dependencies: "@webassemblyjs/ast" "1.11.1" @@ -2323,7 +2779,7 @@ "@webassemblyjs/wasm-parser@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz" integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== dependencies: "@webassemblyjs/ast" "1.11.1" @@ -2335,7 +2791,7 @@ "@webassemblyjs/wast-printer@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" + resolved "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz" integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== dependencies: "@webassemblyjs/ast" "1.11.1" @@ -2343,22 +2799,22 @@ "@xtuc/ieee754@^1.2.0": version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + resolved "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz" integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== "@xtuc/long@4.2.2": version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== abab@^2.0.3, abab@^2.0.5: version "2.0.5" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" + resolved "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz" integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== dependencies: mime-types "~2.1.34" @@ -2366,7 +2822,7 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: acorn-globals@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" + resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz" integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== dependencies: acorn "^7.1.1" @@ -2374,17 +2830,17 @@ acorn-globals@^6.0.0: acorn-import-assertions@^1.7.6: version "1.8.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" + resolved "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz" integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== -acorn-jsx@^5.3.1: +acorn-jsx@^5.3.2: version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-node@^1.6.1: version "1.8.2" - resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" + resolved "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz" integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== dependencies: acorn "^7.0.0" @@ -2393,27 +2849,32 @@ acorn-node@^1.6.1: acorn-walk@^7.0.0, acorn-walk@^7.1.1: version "7.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" + resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== acorn@^7.0.0, acorn@^7.1.1: version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0: +acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0: version "8.7.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz" integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== +acorn@^8.7.1: + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== + address@^1.0.1, address@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" + resolved "https://registry.npmjs.org/address/-/address-1.1.2.tgz" integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== adjust-sourcemap-loader@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" + resolved "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz" integrity sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A== dependencies: loader-utils "^2.0.0" @@ -2421,14 +2882,14 @@ adjust-sourcemap-loader@^4.0.0: agent-base@6: version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: debug "4" aggregate-error@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== dependencies: clean-stack "^2.0.0" @@ -2436,7 +2897,7 @@ aggregate-error@^3.0.0: airbnb-prop-types@^2.16.0: version "2.16.0" - resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2" + resolved "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz" integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg== dependencies: array.prototype.find "^2.1.1" @@ -2451,26 +2912,26 @@ airbnb-prop-types@^2.16.0: ajv-formats@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz" integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== dependencies: ajv "^8.0.0" ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== ajv-keywords@^5.0.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz" integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== dependencies: fast-deep-equal "^3.1.3" ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: fast-deep-equal "^3.1.1" @@ -2480,7 +2941,7 @@ ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: ajv@^8.0.0, ajv@^8.6.0, ajv@^8.8.0: version "8.10.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.10.0.tgz#e573f719bd3af069017e3b66538ab968d040e54d" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz" integrity sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw== dependencies: fast-deep-equal "^3.1.1" @@ -2490,48 +2951,48 @@ ajv@^8.0.0, ajv@^8.6.0, ajv@^8.8.0: ansi-escapes@^4.2.1, ansi-escapes@^4.3.1: version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== dependencies: type-fest "^0.21.3" ansi-html-community@^0.0.8: version "0.0.8" - resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + resolved "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz" integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== ansi-regex@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz" integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== ansi-styles@^3.2.1: version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" ansi-styles@^5.0.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz" integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== dependencies: normalize-path "^3.0.0" @@ -2539,24 +3000,24 @@ anymatch@^3.0.3, anymatch@~3.1.2: arg@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.1.tgz#eb0c9a8f77786cad2af8ff2b862899842d7b6adb" + resolved "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz" integrity sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA== argparse@^1.0.7: version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" argparse@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== aria-query@^4.2.2: version "4.2.2" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz" integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== dependencies: "@babel/runtime" "^7.10.2" @@ -2564,22 +3025,22 @@ aria-query@^4.2.2: aria-query@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz" integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== array-flatten@1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= array-flatten@^2.1.0: version "2.1.2" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz" integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== array-includes@^3.1.3, array-includes@^3.1.4: version "3.1.4" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" + resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz" integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw== dependencies: call-bind "^1.0.2" @@ -2590,12 +3051,12 @@ array-includes@^3.1.3, array-includes@^3.1.4: array-union@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== array.prototype.filter@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/array.prototype.filter/-/array.prototype.filter-1.0.1.tgz#20688792acdb97a09488eaaee9eebbf3966aae21" + resolved "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.1.tgz" integrity sha512-Dk3Ty7N42Odk7PjU/Ci3zT4pLj20YvuVnneG/58ICM6bt4Ij5kZaJTVQ9TSaWaIECX2sFyz4KItkVZqHNnciqw== dependencies: call-bind "^1.0.2" @@ -2606,7 +3067,7 @@ array.prototype.filter@^1.0.0: array.prototype.find@^2.1.1: version "2.1.2" - resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.2.tgz#6abbd0c2573925d8094f7d23112306af8c16d534" + resolved "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.1.2.tgz" integrity sha512-00S1O4ewO95OmmJW7EesWfQlrCrLEL8kZ40w3+GkLX2yTt0m2ggcePPa2uHPJ9KUmJvwRq+lCV9bD8Yim23x/Q== dependencies: call-bind "^1.0.2" @@ -2615,7 +3076,7 @@ array.prototype.find@^2.1.1: array.prototype.flat@^1.2.3, array.prototype.flat@^1.2.5: version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13" + resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz" integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg== dependencies: call-bind "^1.0.2" @@ -2624,7 +3085,7 @@ array.prototype.flat@^1.2.3, array.prototype.flat@^1.2.5: array.prototype.flatmap@^1.2.5: version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz#908dc82d8a406930fdf38598d51e7411d18d4446" + resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz" integrity sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA== dependencies: call-bind "^1.0.0" @@ -2633,44 +3094,44 @@ array.prototype.flatmap@^1.2.5: asap@~2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= ast-types-flow@^0.0.7: version "0.0.7" - resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" + resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz" integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= async@^2.6.2: version "2.6.4" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + resolved "https://registry.npmjs.org/async/-/async-2.6.4.tgz" integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== dependencies: lodash "^4.17.14" async@^3.2.3: version "3.2.3" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" + resolved "https://registry.npmjs.org/async/-/async-3.2.3.tgz" integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== asynckit@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= at-least-node@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== atob@^2.1.2: version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== autoprefixer@^10.4.4: version "10.4.4" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.4.tgz#3e85a245b32da876a893d3ac2ea19f01e7ea5a1e" + resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.4.tgz" integrity sha512-Tm8JxsB286VweiZ5F0anmbyGiNI3v3wGv3mz9W+cxEDYB/6jbnj6GM9H9mK3wIL8ftgl+C07Lcwb8PG5PCCPzA== dependencies: browserslist "^4.20.2" @@ -2682,24 +3143,25 @@ autoprefixer@^10.4.4: axe-core@^4.3.5: version "4.4.1" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413" + resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.4.1.tgz" integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw== -axios@*, axios@^0.26.1: - version "0.26.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" - integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== +axios@*, axios@^0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== dependencies: - follow-redirects "^1.14.8" + follow-redirects "^1.14.9" + form-data "^4.0.0" axobject-query@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" + resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz" integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== babel-jest@^27.4.2, babel-jest@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz" integrity sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg== dependencies: "@jest/transform" "^27.5.1" @@ -2711,9 +3173,22 @@ babel-jest@^27.4.2, babel-jest@^27.5.1: graceful-fs "^4.2.9" slash "^3.0.0" +babel-jest@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-28.1.0.tgz#95a67f8e2e7c0042e7b3ad3951b8af41a533b5ea" + integrity sha512-zNKk0yhDZ6QUwfxh9k07GII6siNGMJWVUU49gmFj5gfdqDKLqa2RArXOF2CODp4Dr7dLxN2cvAV+667dGJ4b4w== + dependencies: + "@jest/transform" "^28.1.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^28.0.2" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + babel-loader@^8.2.3: version "8.2.3" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.3.tgz#8986b40f1a64cacfcb4b8429320085ef68b1342d" + resolved "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz" integrity sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw== dependencies: find-cache-dir "^3.3.1" @@ -2723,14 +3198,14 @@ babel-loader@^8.2.3: babel-plugin-dynamic-import-node@^2.3.3: version "2.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" + resolved "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz" integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== dependencies: object.assign "^4.1.0" babel-plugin-istanbul@^6.1.1: version "6.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz" integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -2741,7 +3216,7 @@ babel-plugin-istanbul@^6.1.1: babel-plugin-jest-hoist@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz" integrity sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ== dependencies: "@babel/template" "^7.3.3" @@ -2749,9 +3224,28 @@ babel-plugin-jest-hoist@^27.5.1: "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" +babel-plugin-jest-hoist@^28.0.2: + version "28.0.2" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.0.2.tgz#9307d03a633be6fc4b1a6bc5c3a87e22bd01dd3b" + integrity sha512-Kizhn/ZL+68ZQHxSnHyuvJv8IchXD62KQxV77TBDV/xoBFBOfgRAk97GNs6hXdTTCiVES9nB2I6+7MXXrk5llQ== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-plugin-macros@^2.6.1: + version "2.8.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" + integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== + dependencies: + "@babel/runtime" "^7.7.2" + cosmiconfig "^6.0.0" + resolve "^1.12.0" + babel-plugin-macros@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" + resolved "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz" integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== dependencies: "@babel/runtime" "^7.12.5" @@ -2760,12 +3254,12 @@ babel-plugin-macros@^3.1.0: babel-plugin-named-asset-import@^0.3.8: version "0.3.8" - resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz#6b7fa43c59229685368683c28bc9734f24524cc2" + resolved "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz" integrity sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q== babel-plugin-polyfill-corejs2@^0.3.0: version "0.3.1" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz#440f1b70ccfaabc6b676d196239b138f8a2cfba5" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz" integrity sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w== dependencies: "@babel/compat-data" "^7.13.11" @@ -2774,7 +3268,7 @@ babel-plugin-polyfill-corejs2@^0.3.0: babel-plugin-polyfill-corejs3@^0.5.0: version "0.5.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz#aabe4b2fa04a6e038b688c5e55d44e78cd3a5f72" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz" integrity sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ== dependencies: "@babel/helper-define-polyfill-provider" "^0.3.1" @@ -2782,14 +3276,14 @@ babel-plugin-polyfill-corejs3@^0.5.0: babel-plugin-polyfill-regenerator@^0.3.0: version "0.3.1" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz#2c0678ea47c75c8cc2fbb1852278d8fb68233990" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz" integrity sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A== dependencies: "@babel/helper-define-polyfill-provider" "^0.3.1" "babel-plugin-styled-components@>= 1.12.0": version "2.0.6" - resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.6.tgz#6f76c7f7224b7af7edc24a4910351948c691fc90" + resolved "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.6.tgz" integrity sha512-Sk+7o/oa2HfHv3Eh8sxoz75/fFvEdHsXV4grdeHufX0nauCmymlnN0rGhIvfpMQSJMvGutJ85gvCGea4iqmDpg== dependencies: "@babel/helper-annotate-as-pure" "^7.16.0" @@ -2800,17 +3294,17 @@ babel-plugin-polyfill-regenerator@^0.3.0: babel-plugin-syntax-jsx@^6.18.0: version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" + resolved "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz" integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY= babel-plugin-transform-react-remove-prop-types@^0.4.24: version "0.4.24" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" + resolved "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz" integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA== babel-preset-current-node-syntax@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz" integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" @@ -2828,15 +3322,23 @@ babel-preset-current-node-syntax@^1.0.0: babel-preset-jest@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz" integrity sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag== dependencies: babel-plugin-jest-hoist "^27.5.1" babel-preset-current-node-syntax "^1.0.0" +babel-preset-jest@^28.0.2: + version "28.0.2" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-28.0.2.tgz#d8210fe4e46c1017e9fa13d7794b166e93aa9f89" + integrity sha512-sYzXIdgIXXroJTFeB3S6sNDWtlJ2dllCdTEsnZ65ACrMojj3hVNFRmnJ1HZtomGi+Be7aqpY/HJ92fr8OhKVkQ== + dependencies: + babel-plugin-jest-hoist "^28.0.2" + babel-preset-current-node-syntax "^1.0.0" + babel-preset-react-app@^10.0.1: version "10.0.1" - resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz#ed6005a20a24f2c88521809fa9aea99903751584" + resolved "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz" integrity sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg== dependencies: "@babel/core" "^7.16.0" @@ -2858,22 +3360,22 @@ babel-preset-react-app@^10.0.1: balanced-match@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base64-js@^1.3.1: version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== batch@0.6.1: version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= bfj@^7.0.2: version "7.0.2" - resolved "https://registry.yarnpkg.com/bfj/-/bfj-7.0.2.tgz#1988ce76f3add9ac2913fd8ba47aad9e651bfbb2" + resolved "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz" integrity sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw== dependencies: bluebird "^3.5.5" @@ -2883,22 +3385,22 @@ bfj@^7.0.2: big.js@^5.2.2: version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== binary-extensions@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== bluebird@^3.5.5: version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== body-parser@1.19.2: version "1.19.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz" integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw== dependencies: bytes "3.1.2" @@ -2914,7 +3416,7 @@ body-parser@1.19.2: bonjour@^3.5.0: version "3.5.0" - resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + resolved "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz" integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= dependencies: array-flatten "^2.1.0" @@ -2926,17 +3428,17 @@ bonjour@^3.5.0: boolbase@^1.0.0, boolbase@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= bootstrap@5.1.3: version "5.1.3" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.1.3.tgz#ba081b0c130f810fa70900acbc1c6d3c28fa8f34" + resolved "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz" integrity sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q== brace-expansion@^1.1.7: version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" @@ -2944,26 +3446,26 @@ brace-expansion@^1.1.7: brace-expansion@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== dependencies: balanced-match "^1.0.0" braces@^3.0.1, braces@~3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== dependencies: fill-range "^7.0.1" browser-process-hrtime@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" + resolved "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4.17.5, browserslist@^4.18.1, browserslist@^4.19.1, browserslist@^4.20.2: version "4.20.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.2.tgz#567b41508757ecd904dab4d1c646c612cd3d4f88" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz" integrity sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA== dependencies: caniuse-lite "^1.0.30001317" @@ -2974,24 +3476,24 @@ browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4 bser@2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== dependencies: node-int64 "^0.4.0" buffer-from@^1.0.0: version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== buffer-indexof@^1.0.0: version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" + resolved "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz" integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== buffer@^6.0.3: version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== dependencies: base64-js "^1.3.1" @@ -2999,22 +3501,22 @@ buffer@^6.0.3: builtin-modules@^3.1.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" + resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz" integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== bytes@3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= bytes@3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== dependencies: function-bind "^1.1.1" @@ -3022,12 +3524,12 @@ call-bind@^1.0.0, call-bind@^1.0.2: callsites@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== camel-case@^4.1.2: version "4.1.2" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + resolved "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz" integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== dependencies: pascal-case "^3.1.2" @@ -3035,27 +3537,27 @@ camel-case@^4.1.2: camelcase-css@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" + resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== camelcase@^5.3.1: version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== camelcase@^6.2.0, camelcase@^6.2.1: version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== camelize@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" + resolved "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz" integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= caniuse-api@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + resolved "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz" integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== dependencies: browserslist "^4.0.0" @@ -3065,17 +3567,17 @@ caniuse-api@^3.0.0: caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001317: version "1.0.30001319" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001319.tgz#eb4da4eb3ecdd409f7ba1907820061d56096e88f" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001319.tgz" integrity sha512-xjlIAFHucBRSMUo1kb5D4LYgcN1M45qdKP++lhqowDpwJwGkpIRTt5qQqnhxjj1vHcI7nrJxWhCC1ATrCEBTcw== case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" - resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" + resolved "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz" integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw== chalk@^2.0.0, chalk@^2.4.1: version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: ansi-styles "^3.2.1" @@ -3084,7 +3586,7 @@ chalk@^2.0.0, chalk@^2.4.1: chalk@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + resolved "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz" integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== dependencies: ansi-styles "^4.1.0" @@ -3092,7 +3594,7 @@ chalk@^3.0.0: chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" @@ -3100,27 +3602,27 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: char-regex@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== char-regex@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-2.0.1.tgz#6dafdb25f9d3349914079f010ba8d0e6ff9cd01e" + resolved "https://registry.npmjs.org/char-regex/-/char-regex-2.0.1.tgz" integrity sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw== charcodes@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/charcodes/-/charcodes-0.2.0.tgz#5208d327e6cc05f99eb80ffc814707572d1f14e4" + resolved "https://registry.npmjs.org/charcodes/-/charcodes-0.2.0.tgz" integrity sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ== check-types@^11.1.1: version "11.1.2" - resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f" + resolved "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz" integrity sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ== cheerio-select@^1.5.0: version "1.5.0" - resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.5.0.tgz#faf3daeb31b17c5e1a9dabcee288aaf8aafa5823" + resolved "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz" integrity sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg== dependencies: css-select "^4.1.3" @@ -3131,7 +3633,7 @@ cheerio-select@^1.5.0: cheerio@^1.0.0-rc.3: version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.10.tgz#2ba3dcdfcc26e7956fc1f440e61d51c643379f3e" + resolved "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz" integrity sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw== dependencies: cheerio-select "^1.5.0" @@ -3144,7 +3646,7 @@ cheerio@^1.0.0-rc.3: chokidar@^3.4.2, chokidar@^3.5.3: version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== dependencies: anymatch "~3.1.2" @@ -3159,53 +3661,58 @@ chokidar@^3.4.2, chokidar@^3.5.3: chrome-trace-event@^1.0.2: version "1.0.3" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz" integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== ci-info@^3.2.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz" integrity sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw== cjs-module-lexer@^1.0.0: version "1.2.2" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" + resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== classnames@^2.2.0, classnames@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz" integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== clean-css@^5.2.2: version "5.2.4" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.2.4.tgz#982b058f8581adb2ae062520808fb2429bd487a4" + resolved "https://registry.npmjs.org/clean-css/-/clean-css-5.2.4.tgz" integrity sha512-nKseG8wCzEuji/4yrgM/5cthL9oTDc5UOQyFMvW/Q53oP6gLH690o1NbuTh6Y18nujr7BxlsFuS7gXLnLzKJGg== dependencies: source-map "~0.6.0" clean-stack@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== cliui@^7.0.2: version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== dependencies: string-width "^4.2.0" strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +clsx@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" + integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== + co@^4.6.0: version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= coa@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" + resolved "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz" integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== dependencies: "@types/q" "^1.5.1" @@ -3214,90 +3721,90 @@ coa@^2.0.2: collect-v8-coverage@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" + resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz" integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== color-convert@^1.9.0: version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== dependencies: color-name "1.1.3" color-convert@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: color-name "~1.1.4" color-name@1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= color-name@^1.1.4, color-name@~1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== colord@^2.9.1: version "2.9.2" - resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.2.tgz#25e2bacbbaa65991422c07ea209e2089428effb1" + resolved "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz" integrity sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ== colorette@^2.0.10: version "2.0.16" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" + resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz" integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== combined-stream@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" commander@^2.19.0, commander@^2.20.0: version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== commander@^7.2.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== commander@^8.3.0: version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== common-path-prefix@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" + resolved "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz" integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== common-tags@^1.8.0: version "1.8.2" - resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" + resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz" integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== commondir@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= compressible@~2.0.16: version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz" integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== dependencies: mime-db ">= 1.43.0 < 2" compression@^1.7.4: version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + resolved "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz" integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== dependencies: accepts "~1.3.5" @@ -3310,56 +3817,56 @@ compression@^1.7.4: compute-scroll-into-view@^1.0.17: version "1.0.17" - resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz#6a88f18acd9d42e9cf4baa6bec7e0522607ab7ab" + resolved "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz" integrity sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg== concat-map@0.0.1: version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= confusing-browser-globals@^1.0.11: version "1.0.11" - resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" + resolved "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz" integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== connect-history-api-fallback@^1.6.0: version "1.6.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" + resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz" integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== content-disposition@0.5.4: version "0.5.4" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== dependencies: safe-buffer "5.2.1" content-type@~1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: +convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.8.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz" integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== dependencies: safe-buffer "~5.1.1" cookie-signature@1.0.6: version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= cookie@0.4.2: version "0.4.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== core-js-compat@^3.20.2, core-js-compat@^3.21.0: version "3.21.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.21.1.tgz#cac369f67c8d134ff8f9bd1623e3bc2c42068c82" + resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.1.tgz" integrity sha512-gbgX5AUvMb8gwxC7FLVWYT7Kkgu/y7+h/h1X43yJkNqhlK2fuYyQimqvKGNZFAY6CKii/GFKJ2cp/1/42TN36g== dependencies: browserslist "^4.19.1" @@ -3367,22 +3874,22 @@ core-js-compat@^3.20.2, core-js-compat@^3.21.0: core-js-pure@^3.20.2, core-js-pure@^3.8.1: version "3.21.1" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.21.1.tgz#8c4d1e78839f5f46208de7230cebfb72bc3bdb51" + resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.21.1.tgz" integrity sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ== core-js@^3.19.2: version "3.21.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94" + resolved "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz" integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig== core-util-is@~1.0.0: version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== cosmiconfig@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz" integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== dependencies: "@types/parse-json" "^4.0.0" @@ -3393,7 +3900,7 @@ cosmiconfig@^6.0.0: cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: version "7.0.1" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz" integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== dependencies: "@types/parse-json" "^4.0.0" @@ -3404,7 +3911,7 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: path-key "^3.1.0" @@ -3413,38 +3920,45 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: crypto-random-string@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== css-blank-pseudo@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz#36523b01c12a25d812df343a32c322d2a2324561" + resolved "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz" integrity sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ== dependencies: postcss-selector-parser "^6.0.9" +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + css-color-keywords@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" + resolved "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz" integrity sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU= css-declaration-sorter@^6.0.3: version "6.1.4" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.1.4.tgz#b9bfb4ed9a41f8dcca9bf7184d849ea94a8294b4" + resolved "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.1.4.tgz" integrity sha512-lpfkqS0fctcmZotJGhnxkIyJWvBXgpyi2wsFd4J8VB7wzyrT6Ch/3Q+FMNJpjK4gu1+GN5khOnpU2ZVKrLbhCw== dependencies: timsort "^0.3.0" css-has-pseudo@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz#57f6be91ca242d5c9020ee3e51bbb5b89fc7af73" + resolved "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz" integrity sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw== dependencies: postcss-selector-parser "^6.0.9" css-loader@^6.5.1: version "6.7.1" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.1.tgz#e98106f154f6e1baf3fc3bc455cb9981c1d5fd2e" + resolved "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz" integrity sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw== dependencies: icss-utils "^5.1.0" @@ -3458,7 +3972,7 @@ css-loader@^6.5.1: css-minimizer-webpack-plugin@^3.2.0: version "3.4.1" - resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz#ab78f781ced9181992fe7b6e4f3422e76429878f" + resolved "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz" integrity sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q== dependencies: cssnano "^5.0.6" @@ -3470,17 +3984,17 @@ css-minimizer-webpack-plugin@^3.2.0: css-prefers-color-scheme@^6.0.3: version "6.0.3" - resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz#ca8a22e5992c10a5b9d315155e7caee625903349" + resolved "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz" integrity sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA== css-select-base-adapter@^0.1.1: version "0.1.1" - resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" + resolved "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz" integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== css-select@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" + resolved "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz" integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== dependencies: boolbase "^1.0.0" @@ -3490,7 +4004,7 @@ css-select@^2.0.0: css-select@^4.1.3: version "4.2.1" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.2.1.tgz#9e665d6ae4c7f9d65dbe69d0316e3221fb274cdd" + resolved "https://registry.npmjs.org/css-select/-/css-select-4.2.1.tgz" integrity sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ== dependencies: boolbase "^1.0.0" @@ -3501,7 +4015,7 @@ css-select@^4.1.3: css-to-react-native@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.0.0.tgz#62dbe678072a824a689bcfee011fc96e02a7d756" + resolved "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz" integrity sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ== dependencies: camelize "^1.0.0" @@ -3510,7 +4024,7 @@ css-to-react-native@^3.0.0: css-tree@1.0.0-alpha.37: version "1.0.0-alpha.37" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz" integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== dependencies: mdn-data "2.0.4" @@ -3518,7 +4032,7 @@ css-tree@1.0.0-alpha.37: css-tree@^1.1.2, css-tree@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz" integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== dependencies: mdn-data "2.0.14" @@ -3526,22 +4040,22 @@ css-tree@^1.1.2, css-tree@^1.1.3: css-what@^3.2.1: version "3.4.2" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" + resolved "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz" integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== css-what@^5.0.1, css-what@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" + resolved "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz" integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== css.escape@^1.5.1: version "1.5.1" - resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + resolved "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz" integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= css@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d" + resolved "https://registry.npmjs.org/css/-/css-3.0.0.tgz" integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ== dependencies: inherits "^2.0.4" @@ -3550,17 +4064,17 @@ css@^3.0.0: cssdb@^6.5.0: version "6.5.0" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-6.5.0.tgz#61264b71f29c834f09b59cb3e5b43c8226590122" + resolved "https://registry.npmjs.org/cssdb/-/cssdb-6.5.0.tgz" integrity sha512-Rh7AAopF2ckPXe/VBcoUS9JrCZNSyc60+KpgE6X25vpVxA32TmiqvExjkfhwP4wGSb6Xe8Z/JIyGqwgx/zZYFA== cssesc@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== cssnano-preset-default@^*: version "5.2.4" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.4.tgz#eced79bbc1ab7270337c4038a21891daac2329bc" + resolved "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.4.tgz" integrity sha512-w1Gg8xsebln6/axZ6qDFQHuglrGfbIHOIx0g4y9+etRlRab8CGpSpe6UMsrgJe4zhCaJ0LwLmc+PhdLRTwnhIA== dependencies: css-declaration-sorter "^6.0.3" @@ -3595,12 +4109,12 @@ cssnano-preset-default@^*: cssnano-utils@^*, cssnano-utils@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz#95684d08c91511edfc70d2636338ca37ef3a6861" + resolved "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz" integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== cssnano@^5.0.6: version "5.1.4" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.4.tgz#c648192e8e2f1aacb7d839e6aa3706b50cc7f8e4" + resolved "https://registry.npmjs.org/cssnano/-/cssnano-5.1.4.tgz" integrity sha512-hbfhVZreEPyzl+NbvRsjNo54JOX80b+j6nqG2biLVLaZHJEiqGyMh4xDGHtwhUKd5p59mj2GlDqlUBwJUuIu5A== dependencies: cssnano-preset-default "^*" @@ -3609,41 +4123,41 @@ cssnano@^5.0.6: csso@^4.0.2, csso@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + resolved "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz" integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== dependencies: css-tree "^1.1.2" cssom@^0.4.4: version "0.4.4" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" + resolved "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz" integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== cssom@~0.3.6: version "0.3.8" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz" integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== cssstyle@^2.3.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz" integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== dependencies: cssom "~0.3.6" csstype@^3.0.2: version "3.0.11" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz" integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw== damerau-levenshtein@^1.0.7: version "1.0.8" - resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" + resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== data-urls@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" + resolved "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz" integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== dependencies: abab "^2.0.3" @@ -3652,43 +4166,43 @@ data-urls@^2.0.0: debug@2.6.9, debug@^2.6.0, debug@^2.6.9: version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" debug@^3.1.1, debug@^3.2.7: version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" decimal.js@^10.2.1: version "10.3.1" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" + resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz" integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== decode-uri-component@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= dedent@^0.7.0: version "0.7.0" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= deep-equal@^1.0.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" + resolved "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz" integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== dependencies: is-arguments "^1.0.4" @@ -3700,41 +4214,41 @@ deep-equal@^1.0.1: deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== deepmerge@^4.2.2: version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== default-gateway@^6.0.3: version "6.0.3" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" + resolved "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz" integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== dependencies: execa "^5.0.0" define-lazy-prop@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== define-properties@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz" integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== dependencies: object-keys "^1.0.12" defined@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + resolved "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz" integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= del@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" + resolved "https://registry.npmjs.org/del/-/del-6.0.0.tgz" integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ== dependencies: globby "^11.0.1" @@ -3748,37 +4262,37 @@ del@^6.0.0: delayed-stream@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= depd@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= dequal@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d" + resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz" integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug== destroy@~1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= detect-newline@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== detect-node@^2.0.4: version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== detect-port-alt@^1.1.6: version "1.1.6" - resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" + resolved "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz" integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== dependencies: address "^1.0.1" @@ -3786,7 +4300,7 @@ detect-port-alt@^1.1.6: detective@^5.2.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b" + resolved "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz" integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg== dependencies: acorn-node "^1.6.1" @@ -3795,39 +4309,44 @@ detective@^5.2.0: didyoumean@^1.2.2: version "1.2.2" - resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" + resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== diff-sequences@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz" integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== +diff-sequences@^28.0.2: + version "28.0.2" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.0.2.tgz#40f8d4ffa081acbd8902ba35c798458d0ff1af41" + integrity sha512-YtEoNynLDFCRznv/XDalsKGSZDoj0U5kLnXvY0JSq3nBboRrZXjD81+eSiwi+nzcZDwedMmcowcxNwwgFW23mQ== + dir-glob@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== dependencies: path-type "^4.0.0" discontinuous-range@1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" + resolved "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz" integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo= dlv@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" + resolved "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz" integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== dns-equal@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + resolved "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz" integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= dns-packet@^1.3.1: version "1.3.4" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.4.tgz#e3455065824a2507ba886c55a89963bb107dec6f" + resolved "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz" integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA== dependencies: ip "^1.1.0" @@ -3835,40 +4354,40 @@ dns-packet@^1.3.1: dns-txt@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + resolved "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz" integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= dependencies: buffer-indexof "^1.0.0" doctrine@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== dependencies: esutils "^2.0.2" doctrine@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== dependencies: esutils "^2.0.2" dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: version "0.5.13" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.13.tgz#102ee5f25eacce09bdf1cfa5a298f86da473be4b" + resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.13.tgz" integrity sha512-R305kwb5CcMDIpSHUnLyIAp7SrSPBx6F0VfQFB3M75xVMHhXJJIdePYgbPPh1o57vCHNu5QztokWUPsLjWzFqw== dom-converter@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz" integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== dependencies: utila "~0.4" dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1: version "5.2.1" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz" integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== dependencies: "@babel/runtime" "^7.8.7" @@ -3876,7 +4395,7 @@ dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1: dom-serializer@0: version "0.2.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz" integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== dependencies: domelementtype "^2.0.1" @@ -3884,7 +4403,7 @@ dom-serializer@0: dom-serializer@^1.0.1, dom-serializer@^1.3.2: version "1.3.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz" integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== dependencies: domelementtype "^2.0.1" @@ -3893,31 +4412,31 @@ dom-serializer@^1.0.1, dom-serializer@^1.3.2: domelementtype@1: version "1.3.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== domelementtype@^2.0.1, domelementtype@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz" integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== domexception@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" + resolved "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz" integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg== dependencies: webidl-conversions "^5.0.0" domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.0: version "4.3.1" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz" integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== dependencies: domelementtype "^2.2.0" domutils@^1.7.0: version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + resolved "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz" integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== dependencies: dom-serializer "0" @@ -3925,7 +4444,7 @@ domutils@^1.7.0: domutils@^2.5.2, domutils@^2.7.0, domutils@^2.8.0: version "2.8.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + resolved "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz" integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== dependencies: dom-serializer "^1.0.1" @@ -3934,7 +4453,7 @@ domutils@^2.5.2, domutils@^2.7.0, domutils@^2.8.0: dot-case@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz" integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== dependencies: no-case "^3.0.4" @@ -3942,64 +4461,69 @@ dot-case@^3.0.4: dotenv-expand@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" + resolved "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz" integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== dotenv@^10.0.0: version "10.0.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== duplexer@^0.1.2: version "0.1.2" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== ee-first@1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= ejs@^3.1.6: version "3.1.7" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.7.tgz#c544d9c7f715783dd92f0bddcf73a59e6962d006" + resolved "https://registry.npmjs.org/ejs/-/ejs-3.1.7.tgz" integrity sha512-BIar7R6abbUxDA3bfXrO4DSgwo8I+fB5/1zgujl3HLLjwd6+9iOnrT+t3grn2qbk9vOgBubXOFwX2m9axoFaGw== dependencies: jake "^10.8.5" electron-to-chromium@^1.4.84: version "1.4.88" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.88.tgz#ebe6a2573b563680c7a7bf3a51b9e465c9c501db" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.88.tgz" integrity sha512-oA7mzccefkvTNi9u7DXmT0LqvhnOiN2BhSrKerta7HeUC1cLoIwtbf2wL+Ah2ozh5KQd3/1njrGrwDBXx6d14Q== +emittery@^0.10.2: + version "0.10.2" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.10.2.tgz#902eec8aedb8c41938c46e9385e9db7e03182933" + integrity sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw== + emittery@^0.8.1: version "0.8.1" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" + resolved "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz" integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== emoji-regex@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== emoji-regex@^9.2.2: version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== emojis-list@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + resolved "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== encodeurl@~1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= enhanced-resolve@^5.9.2: version "5.9.2" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz#0224dcd6a43389ebfb2d55efee517e5466772dd9" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz" integrity sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA== dependencies: graceful-fs "^4.2.4" @@ -4007,12 +4531,12 @@ enhanced-resolve@^5.9.2: entities@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== enzyme-adapter-react-16@^1.15.6: version "1.15.6" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.6.tgz#fd677a658d62661ac5afd7f7f541f141f8085901" + resolved "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.6.tgz" integrity sha512-yFlVJCXh8T+mcQo8M6my9sPgeGzj85HSHi6Apgf1Cvq/7EL/J9+1JoJmJsRxZgyTvPMAqOEpRSu/Ii/ZpyOk0g== dependencies: enzyme-adapter-utils "^1.14.0" @@ -4027,7 +4551,7 @@ enzyme-adapter-react-16@^1.15.6: enzyme-adapter-utils@^1.14.0: version "1.14.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.0.tgz#afbb0485e8033aa50c744efb5f5711e64fbf1ad0" + resolved "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.0.tgz" integrity sha512-F/z/7SeLt+reKFcb7597IThpDp0bmzcH1E9Oabqv+o01cID2/YInlqHbFl7HzWBl4h3OdZYedtwNDOmSKkk0bg== dependencies: airbnb-prop-types "^2.16.0" @@ -4040,7 +4564,7 @@ enzyme-adapter-utils@^1.14.0: enzyme-shallow-equal@^1.0.1, enzyme-shallow-equal@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz#b9256cb25a5f430f9bfe073a84808c1d74fced2e" + resolved "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz" integrity sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q== dependencies: has "^1.0.3" @@ -4048,7 +4572,7 @@ enzyme-shallow-equal@^1.0.1, enzyme-shallow-equal@^1.0.4: enzyme@^3.11.0: version "3.11.0" - resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.11.0.tgz#71d680c580fe9349f6f5ac6c775bc3e6b7a79c28" + resolved "https://registry.npmjs.org/enzyme/-/enzyme-3.11.0.tgz" integrity sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw== dependencies: array.prototype.flat "^1.2.3" @@ -4076,21 +4600,21 @@ enzyme@^3.11.0: error-ex@^1.3.1: version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== dependencies: is-arrayish "^0.2.1" error-stack-parser@^2.0.6: version "2.0.7" - resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.7.tgz#b0c6e2ce27d0495cf78ad98715e0cad1219abb57" + resolved "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.7.tgz" integrity sha512-chLOW0ZGRf4s8raLrDxa5sdkvPec5YdvwbFnqJme4rk0rFajP8mPtrDL1+I+CwrQDCjswDA5sREX7jYQDQs9vA== dependencies: stackframe "^1.1.1" es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.19.1: version "1.19.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" + resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz" integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== dependencies: call-bind "^1.0.2" @@ -4116,17 +4640,17 @@ es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.19.1: es-array-method-boxes-properly@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + resolved "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz" integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== es-module-lexer@^0.9.0: version "0.9.3" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" + resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz" integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== es-to-primitive@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz" integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== dependencies: is-callable "^1.1.4" @@ -4135,32 +4659,32 @@ es-to-primitive@^1.2.1: escalade@^3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== escape-html@~1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= escape-string-regexp@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= escape-string-regexp@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== escape-string-regexp@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== escodegen@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" + resolved "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz" integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== dependencies: esprima "^4.0.1" @@ -4172,13 +4696,13 @@ escodegen@^2.0.0: eslint-config-prettier@^8.3.0: version "8.5.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" + resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz" integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== -eslint-config-react-app@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-7.0.0.tgz#0fa96d5ec1dfb99c029b1554362ab3fa1c3757df" - integrity sha512-xyymoxtIt1EOsSaGag+/jmcywRuieQoA2JbPCjnw9HukFj9/97aGPoZVFioaotzk1K5Qt9sHO5EutZbkrAXS0g== +eslint-config-react-app@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz#73ba3929978001c5c86274c017ea57eb5fa644b4" + integrity sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA== dependencies: "@babel/core" "^7.16.0" "@babel/eslint-parser" "^7.16.3" @@ -4197,12 +4721,12 @@ eslint-config-react-app@^7.0.0: eslint-config-standard@^16.0.3: version "16.0.3" - resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz#6c8761e544e96c531ff92642eeb87842b8488516" + resolved "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz" integrity sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg== eslint-import-resolver-node@^0.3.6: version "0.3.6" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" + resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz" integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== dependencies: debug "^3.2.7" @@ -4210,7 +4734,7 @@ eslint-import-resolver-node@^0.3.6: eslint-module-utils@^2.7.2: version "2.7.3" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz#ad7e3a10552fdd0642e1e55292781bd6e34876ee" + resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz" integrity sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ== dependencies: debug "^3.2.7" @@ -4218,7 +4742,7 @@ eslint-module-utils@^2.7.2: eslint-plugin-es@^3.0.0: version "3.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz#75a7cdfdccddc0589934aeeb384175f221c57893" + resolved "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz" integrity sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ== dependencies: eslint-utils "^2.0.0" @@ -4226,7 +4750,7 @@ eslint-plugin-es@^3.0.0: eslint-plugin-flowtype@^8.0.3: version "8.0.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz#e1557e37118f24734aa3122e7536a038d34a4912" + resolved "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz" integrity sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ== dependencies: lodash "^4.17.21" @@ -4234,7 +4758,7 @@ eslint-plugin-flowtype@^8.0.3: eslint-plugin-import@^2.25.3: version "2.25.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz#322f3f916a4e9e991ac7af32032c25ce313209f1" + resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz" integrity sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA== dependencies: array-includes "^3.1.4" @@ -4253,14 +4777,14 @@ eslint-plugin-import@^2.25.3: eslint-plugin-jest@^25.3.0: version "25.7.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz#ff4ac97520b53a96187bad9c9814e7d00de09a6a" + resolved "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz" integrity sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ== dependencies: "@typescript-eslint/experimental-utils" "^5.0.0" eslint-plugin-jsx-a11y@^6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz#cdbf2df901040ca140b6ec14715c988889c2a6d8" + resolved "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz" integrity sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g== dependencies: "@babel/runtime" "^7.16.3" @@ -4278,7 +4802,7 @@ eslint-plugin-jsx-a11y@^6.5.1: eslint-plugin-node@^11.1.0: version "11.1.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d" + resolved "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz" integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g== dependencies: eslint-plugin-es "^3.0.0" @@ -4290,24 +4814,24 @@ eslint-plugin-node@^11.1.0: eslint-plugin-prettier@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz#8b99d1e4b8b24a762472b4567992023619cb98e0" + resolved "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz" integrity sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ== dependencies: prettier-linter-helpers "^1.0.0" eslint-plugin-promise@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz#017652c07c9816413a41e11c30adc42c3d55ff18" + resolved "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz" integrity sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw== eslint-plugin-react-hooks@^4.3.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz#318dbf312e06fab1c835a4abef00121751ac1172" + resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz" integrity sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA== eslint-plugin-react@^7.27.1, eslint-plugin-react@^7.28.0: version "7.29.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz#4717de5227f55f3801a5fd51a16a4fa22b5914d2" + resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz" integrity sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ== dependencies: array-includes "^3.1.4" @@ -4327,19 +4851,19 @@ eslint-plugin-react@^7.27.1, eslint-plugin-react@^7.28.0: eslint-plugin-standard@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-5.0.0.tgz#c43f6925d669f177db46f095ea30be95476b1ee4" + resolved "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-5.0.0.tgz" integrity sha512-eSIXPc9wBM4BrniMzJRBm2uoVuXz2EPa+NXPk2+itrVt+r5SbKFERx/IgrK/HmfjddyKVz2f+j+7gBRvu19xLg== eslint-plugin-testing-library@^5.0.1: version "5.1.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.1.0.tgz#6ad539a53d4e897d3045902f8e534e07cebd4e8b" + resolved "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.1.0.tgz" integrity sha512-YSNzasJUbyhOTe14ZPygeOBvcPvcaNkwHwrj4vdf+uirr2D32JTDaKi6CP5Os2aWtOcvt4uBSPXp9h5xGoqvWQ== dependencies: "@typescript-eslint/utils" "^5.13.0" eslint-scope@5.1.1, eslint-scope@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== dependencies: esrecurse "^4.3.0" @@ -4347,7 +4871,7 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1: eslint-scope@^7.1.1: version "7.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz" integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== dependencies: esrecurse "^4.3.0" @@ -4355,36 +4879,36 @@ eslint-scope@^7.1.1: eslint-utils@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz" integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== dependencies: eslint-visitor-keys "^1.1.0" eslint-utils@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== dependencies: eslint-visitor-keys "^2.0.0" eslint-visitor-keys@^1.1.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz" integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== eslint-visitor-keys@^2.0.0, eslint-visitor-keys@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== eslint-webpack-plugin@^3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/eslint-webpack-plugin/-/eslint-webpack-plugin-3.1.1.tgz#83dad2395e5f572d6f4d919eedaa9cf902890fcb" + resolved "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.1.1.tgz" integrity sha512-xSucskTN9tOkfW7so4EaiFIkulWLXwCB/15H917lR6pTv0Zot6/fetFucmENRb7J5whVSFKIvwnrnsa78SG2yg== dependencies: "@types/eslint" "^7.28.2" @@ -4393,12 +4917,12 @@ eslint-webpack-plugin@^3.1.1: normalize-path "^3.0.0" schema-utils "^3.1.1" -eslint@^8.3.0, eslint@^8.9.0: - version "8.11.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.11.0.tgz#88b91cfba1356fc10bb9eb592958457dfe09fb37" - integrity sha512-/KRpd9mIRg2raGxHRGwW9ZywYNAClZrHjdueHcrVDuO3a6bj83eoTirCCk0M0yPwOjWYKHwRVRid+xK4F/GHgA== +eslint@^8.15.0, eslint@^8.3.0: + version "8.15.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.15.0.tgz#fea1d55a7062da48d82600d2e0974c55612a11e9" + integrity sha512-GG5USZ1jhCu8HJkzGgeK8/+RGnHaNYZGrGDzUtigK3BsGESW/rs2az23XqE0WVwDxy1VRvvjSSGu5nB0Bu+6SA== dependencies: - "@eslint/eslintrc" "^1.2.1" + "@eslint/eslintrc" "^1.2.3" "@humanwhocodes/config-array" "^0.9.2" ajv "^6.10.0" chalk "^4.0.0" @@ -4409,7 +4933,7 @@ eslint@^8.3.0, eslint@^8.9.0: eslint-scope "^7.1.1" eslint-utils "^3.0.0" eslint-visitor-keys "^3.3.0" - espree "^9.3.1" + espree "^9.3.2" esquery "^1.4.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -4425,7 +4949,7 @@ eslint@^8.3.0, eslint@^8.9.0: json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" - minimatch "^3.0.4" + minimatch "^3.1.2" natural-compare "^1.4.0" optionator "^0.9.1" regexpp "^3.2.0" @@ -4434,72 +4958,72 @@ eslint@^8.3.0, eslint@^8.9.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" -espree@^9.3.1: - version "9.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd" - integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ== +espree@^9.3.2: + version "9.3.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.2.tgz#f58f77bd334731182801ced3380a8cc859091596" + integrity sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA== dependencies: - acorn "^8.7.0" - acorn-jsx "^5.3.1" + acorn "^8.7.1" + acorn-jsx "^5.3.2" eslint-visitor-keys "^3.3.0" esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== esquery@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== dependencies: estraverse "^5.1.0" esrecurse@^4.3.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: estraverse "^5.2.0" estraverse@^4.1.1: version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== estree-walker@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz" integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== esutils@^2.0.2: version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== etag@~1.8.1: version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= eventemitter3@^4.0.0: version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== events@^3.2.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== execa@^5.0.0: version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== dependencies: cross-spawn "^7.0.3" @@ -4514,12 +5038,12 @@ execa@^5.0.0: exit@^0.1.2: version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= expect@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/expect/-/expect-27.5.1.tgz#83ce59f1e5bdf5f9d2b94b61d2050db48f3fef74" + resolved "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz" integrity sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw== dependencies: "@jest/types" "^27.5.1" @@ -4527,9 +5051,20 @@ expect@^27.5.1: jest-matcher-utils "^27.5.1" jest-message-util "^27.5.1" +expect@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-28.1.0.tgz#10e8da64c0850eb8c39a480199f14537f46e8360" + integrity sha512-qFXKl8Pmxk8TBGfaFKRtcQjfXEnKAs+dmlxdwvukJZorwrAabT7M3h8oLOG01I2utEhkmUTi17CHaPBovZsKdw== + dependencies: + "@jest/expect-utils" "^28.1.0" + jest-get-type "^28.0.2" + jest-matcher-utils "^28.1.0" + jest-message-util "^28.1.0" + jest-util "^28.1.0" + express@^4.17.1: version "4.17.3" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1" + resolved "https://registry.npmjs.org/express/-/express-4.17.3.tgz" integrity sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg== dependencies: accepts "~1.3.8" @@ -4565,17 +5100,17 @@ express@^4.17.1: fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-diff@^1.1.2: version "1.2.0" - resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" + resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== fast-glob@^3.2.11, fast-glob@^3.2.9: version "3.2.11" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz" integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== dependencies: "@nodelib/fs.stat" "^2.0.2" @@ -4586,45 +5121,45 @@ fast-glob@^3.2.11, fast-glob@^3.2.9: fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= fastq@^1.6.0: version "1.13.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz" integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== dependencies: reusify "^1.0.4" faye-websocket@^0.11.3: version "0.11.4" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz" integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== dependencies: websocket-driver ">=0.5.1" fb-watchman@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" + resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz" integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== dependencies: bser "2.1.1" file-entry-cache@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== dependencies: flat-cache "^3.0.4" file-loader@^6.2.0: version "6.2.0" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" + resolved "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz" integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== dependencies: loader-utils "^2.0.0" @@ -4632,26 +5167,26 @@ file-loader@^6.2.0: filelist@^1.0.1: version "1.0.3" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.3.tgz#448607750376484932f67ef1b9ff07386b036c83" + resolved "https://registry.npmjs.org/filelist/-/filelist-1.0.3.tgz" integrity sha512-LwjCsruLWQULGYKy7TX0OPtrL9kLpojOFKc5VCTxdFTV7w5zbsgqVKfnkKG7Qgjtq50gKfO56hJv88OfcGb70Q== dependencies: minimatch "^5.0.1" filesize@^8.0.6: version "8.0.7" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8" + resolved "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz" integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== fill-range@^7.0.1: version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== dependencies: to-regex-range "^5.0.1" finalhandler@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz" integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== dependencies: debug "2.6.9" @@ -4664,30 +5199,35 @@ finalhandler@~1.1.2: find-cache-dir@^3.3.1: version "3.3.2" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + resolved "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz" integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== dependencies: commondir "^1.0.1" make-dir "^3.0.2" pkg-dir "^4.1.0" +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + find-up@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + resolved "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz" integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= dependencies: locate-path "^2.0.0" find-up@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + resolved "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz" integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== dependencies: locate-path "^3.0.0" find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== dependencies: locate-path "^5.0.0" @@ -4695,7 +5235,7 @@ find-up@^4.0.0, find-up@^4.1.0: find-up@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: locate-path "^6.0.0" @@ -4703,7 +5243,7 @@ find-up@^5.0.0: flat-cache@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== dependencies: flatted "^3.1.0" @@ -4711,17 +5251,17 @@ flat-cache@^3.0.4: flatted@^3.1.0: version "3.2.5" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz" integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== -follow-redirects@^1.0.0, follow-redirects@^1.14.8: - version "1.14.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" - integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== +follow-redirects@^1.0.0, follow-redirects@^1.14.9: + version "1.15.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.0.tgz#06441868281c86d0dda4ad8bdaead2d02dca89d4" + integrity sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ== fork-ts-checker-webpack-plugin@^6.5.0: version "6.5.0" - resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.0.tgz#0282b335fa495a97e167f69018f566ea7d2a2b5e" + resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.0.tgz" integrity sha512-cS178Y+xxtIjEUorcHddKS7yCMlrDPV31mt47blKKRfMd70Kxu5xruAFE2o9sDY6wVC5deuob/u/alD04YYHnw== dependencies: "@babel/code-frame" "^7.8.3" @@ -4740,31 +5280,40 @@ fork-ts-checker-webpack-plugin@^6.5.0: form-data@^3.0.0: version "3.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + resolved "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz" integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== fraction.js@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" + resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz" integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== fresh@0.5.2: version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= fs-extra@^10.0.0: version "10.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz" integrity sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag== dependencies: graceful-fs "^4.2.0" @@ -4773,7 +5322,7 @@ fs-extra@^10.0.0: fs-extra@^9.0.0, fs-extra@^9.0.1: version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== dependencies: at-least-node "^1.0.0" @@ -4783,27 +5332,27 @@ fs-extra@^9.0.0, fs-extra@^9.0.1: fs-monkey@1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" + resolved "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz" integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== function-bind@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== function.prototype.name@^1.1.2, function.prototype.name@^1.1.3: version "1.1.5" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz" integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== dependencies: call-bind "^1.0.2" @@ -4813,27 +5362,27 @@ function.prototype.name@^1.1.2, function.prototype.name@^1.1.3: functional-red-black-tree@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= functions-have-names@^1.2.2: version "1.2.2" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21" + resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.2.tgz" integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA== gensync@^1.0.0-beta.2: version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== get-caller-file@^2.0.5: version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz" integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== dependencies: function-bind "^1.1.1" @@ -4842,22 +5391,22 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" - resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + resolved "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz" integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== get-package-type@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== get-stream@^6.0.0: version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== get-symbol-description@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz" integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== dependencies: call-bind "^1.0.2" @@ -4865,26 +5414,26 @@ get-symbol-description@^1.0.0: glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" glob-parent@^6.0.1, glob-parent@^6.0.2: version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== dependencies: is-glob "^4.0.3" glob-to-regexp@^0.4.1: version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== dependencies: fs.realpath "^1.0.0" @@ -4896,14 +5445,14 @@ glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0: global-modules@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + resolved "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz" integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== dependencies: global-prefix "^3.0.0" global-prefix@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + resolved "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz" integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== dependencies: ini "^1.3.5" @@ -4912,19 +5461,19 @@ global-prefix@^3.0.0: globals@^11.1.0: version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.6.0, globals@^13.9.0: version "13.13.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.13.0.tgz#ac32261060d8070e2719dd6998406e27d2b5727b" + resolved "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz" integrity sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A== dependencies: type-fest "^0.20.2" -globby@^11.0.1, globby@^11.0.4: +globby@^11.0.1, globby@^11.0.4, globby@^11.1.0: version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== dependencies: array-union "^2.1.0" @@ -4936,87 +5485,87 @@ globby@^11.0.1, globby@^11.0.4: graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.9" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== gzip-size@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" + resolved "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz" integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== dependencies: duplexer "^0.1.2" handle-thing@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz" integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== harmony-reflect@^1.4.6: version "1.6.2" - resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.2.tgz#31ecbd32e648a34d030d86adb67d4d47547fe710" + resolved "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz" integrity sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g== has-bigints@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" + resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz" integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== has-flag@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= has-flag@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== has-tostringtag@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz" integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== dependencies: has-symbols "^1.0.2" has@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== dependencies: function-bind "^1.1.1" he@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== history@^5.2.0: version "5.3.0" - resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b" + resolved "https://registry.npmjs.org/history/-/history-5.3.0.tgz" integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ== dependencies: "@babel/runtime" "^7.7.6" -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== dependencies: react-is "^16.7.0" hoopy@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" + resolved "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz" integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== hpack.js@^2.1.6: version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + resolved "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz" integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= dependencies: inherits "^2.0.1" @@ -5026,7 +5575,7 @@ hpack.js@^2.1.6: html-element-map@^1.2.0: version "1.3.1" - resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.3.1.tgz#44b2cbcfa7be7aa4ff59779e47e51012e1c73c08" + resolved "https://registry.npmjs.org/html-element-map/-/html-element-map-1.3.1.tgz" integrity sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg== dependencies: array.prototype.filter "^1.0.0" @@ -5034,24 +5583,24 @@ html-element-map@^1.2.0: html-encoding-sniffer@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" + resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz" integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== dependencies: whatwg-encoding "^1.0.5" html-entities@^2.1.0, html-entities@^2.3.2: version "2.3.2" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.2.tgz#760b404685cb1d794e4f4b744332e3b00dcfe488" + resolved "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz" integrity sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ== html-escaper@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== html-minifier-terser@^6.0.2: version "6.1.0" - resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab" + resolved "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz" integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw== dependencies: camel-case "^4.1.2" @@ -5064,7 +5613,7 @@ html-minifier-terser@^6.0.2: html-webpack-plugin@^5.5.0: version "5.5.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz#c3911936f57681c1f9f4d8b68c158cd9dfe52f50" + resolved "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz" integrity sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw== dependencies: "@types/html-minifier-terser" "^6.0.0" @@ -5075,7 +5624,7 @@ html-webpack-plugin@^5.5.0: htmlparser2@^6.1.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz" integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== dependencies: domelementtype "^2.0.1" @@ -5085,12 +5634,12 @@ htmlparser2@^6.1.0: http-deceiver@^1.2.7: version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= http-errors@1.8.1: version "1.8.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz" integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== dependencies: depd "~1.1.2" @@ -5101,7 +5650,7 @@ http-errors@1.8.1: http-errors@~1.6.2: version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= dependencies: depd "~1.1.2" @@ -5111,12 +5660,12 @@ http-errors@~1.6.2: http-parser-js@>=0.5.1: version "0.5.6" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.6.tgz#2e02406ab2df8af8a7abfba62e0da01c62b95afd" + resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.6.tgz" integrity sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA== http-proxy-agent@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz" integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== dependencies: "@tootallnate/once" "1" @@ -5125,7 +5674,7 @@ http-proxy-agent@^4.0.1: http-proxy-middleware@^2.0.0: version "2.0.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz#03af0f4676d172ae775cb5c33f592f40e1a4e07a" + resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz" integrity sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg== dependencies: "@types/http-proxy" "^1.17.8" @@ -5136,7 +5685,7 @@ http-proxy-middleware@^2.0.0: http-proxy@^1.18.1: version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz" integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== dependencies: eventemitter3 "^4.0.0" @@ -5145,7 +5694,7 @@ http-proxy@^1.18.1: https-proxy-agent@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz" integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== dependencies: agent-base "6" @@ -5153,58 +5702,58 @@ https-proxy-agent@^5.0.0: human-signals@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== iconv-lite@0.4.24: version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" iconv-lite@^0.6.3: version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: safer-buffer ">= 2.1.2 < 3.0.0" icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== idb@^6.1.4: version "6.1.5" - resolved "https://registry.yarnpkg.com/idb/-/idb-6.1.5.tgz#dbc53e7adf1ac7c59f9b2bf56e00b4ea4fce8c7b" + resolved "https://registry.npmjs.org/idb/-/idb-6.1.5.tgz" integrity sha512-IJtugpKkiVXQn5Y+LteyBCNk1N8xpGV3wWZk9EVtZWH8DYkjBn0bX1XnGP9RkyZF0sAcywa6unHqSWKe7q4LGw== identity-obj-proxy@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14" + resolved "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz" integrity sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ= dependencies: harmony-reflect "^1.4.6" ieee754@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.1.1, ignore@^5.1.8, ignore@^5.2.0: +ignore@^5.1.1, ignore@^5.2.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== immer@^9.0.7: version "9.0.12" - resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20" + resolved "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz" integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA== import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== dependencies: parent-module "^1.0.0" @@ -5212,7 +5761,7 @@ import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: import-local@^3.0.2: version "3.1.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + resolved "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz" integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== dependencies: pkg-dir "^4.2.0" @@ -5220,17 +5769,17 @@ import-local@^3.0.2: imurmurhash@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= indent-string@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== inflight@^1.0.4: version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= dependencies: once "^1.3.0" @@ -5238,22 +5787,22 @@ inflight@^1.0.4: inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== inherits@2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ini@^1.3.5: version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== internal-slot@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz" integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== dependencies: get-intrinsic "^1.1.0" @@ -5262,29 +5811,29 @@ internal-slot@^1.0.3: invariant@^2.2.1, invariant@^2.2.4: version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== dependencies: loose-envify "^1.0.0" ip@^1.1.0: version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + resolved "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz" integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= ipaddr.js@1.9.1: version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== ipaddr.js@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz" integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== is-arguments@^1.0.4: version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + resolved "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz" integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== dependencies: call-bind "^1.0.2" @@ -5292,26 +5841,26 @@ is-arguments@^1.0.4: is-arrayish@^0.2.1: version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= is-bigint@^1.0.1: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== dependencies: has-bigints "^1.0.1" is-binary-path@~2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== dependencies: binary-extensions "^2.0.0" is-boolean-object@^1.0.1, is-boolean-object@^1.1.0: version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz" integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== dependencies: call-bind "^1.0.2" @@ -5319,100 +5868,100 @@ is-boolean-object@^1.0.1, is-boolean-object@^1.1.0: is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.4: version "1.2.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz" integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== is-core-module@^2.2.0, is-core-module@^2.8.0, is-core-module@^2.8.1: version "2.8.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz" integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== dependencies: has "^1.0.3" is-date-object@^1.0.1: version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz" integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== dependencies: has-tostringtag "^1.0.0" is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== is-extglob@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= is-fullwidth-code-point@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== is-generator-fn@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" is-module@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz" integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= is-negative-zero@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz" integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== is-number-object@^1.0.4: version "1.0.6" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" + resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz" integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== dependencies: has-tostringtag "^1.0.0" is-number@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== is-obj@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + resolved "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz" integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= is-path-cwd@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" + resolved "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz" integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== is-path-inside@^3.0.2: version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== is-plain-obj@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz" integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== is-potential-custom-element-name@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== dependencies: call-bind "^1.0.2" @@ -5420,80 +5969,80 @@ is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.4: is-regexp@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + resolved "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz" integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= is-root@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" + resolved "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz" integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== is-shared-array-buffer@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" + resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz" integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== is-stream@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== dependencies: has-tostringtag "^1.0.0" is-subset@^0.1.1: version "0.1.1" - resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" + resolved "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz" integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY= is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz" integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== dependencies: has-symbols "^1.0.2" is-typedarray@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= is-weakref@^1.0.1: version "1.0.2" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz" integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== dependencies: call-bind "^1.0.2" is-wsl@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== dependencies: is-docker "^2.0.0" isarray@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= isexe@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz" integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz#7b49198b657b27a730b8e9cb601f1e1bff24c59a" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz" integrity sha512-czwUz525rkOFDJxfKK6mYfIs9zBKILyrZQxjz3ABhjQXhbhFsSbo1HW/BFcsDnfJYJWA6thRR5/TUY2qs5W99Q== dependencies: "@babel/core" "^7.12.3" @@ -5504,7 +6053,7 @@ istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: istanbul-lib-report@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== dependencies: istanbul-lib-coverage "^3.0.0" @@ -5513,7 +6062,7 @@ istanbul-lib-report@^3.0.0: istanbul-lib-source-maps@^4.0.0: version "4.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz" integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== dependencies: debug "^4.1.1" @@ -5522,7 +6071,7 @@ istanbul-lib-source-maps@^4.0.0: istanbul-reports@^3.1.3: version "3.1.4" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.4.tgz#1b6f068ecbc6c331040aab5741991273e609e40c" + resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz" integrity sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw== dependencies: html-escaper "^2.0.0" @@ -5530,7 +6079,7 @@ istanbul-reports@^3.1.3: jake@^10.8.5: version "10.8.5" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" + resolved "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz" integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== dependencies: async "^3.2.3" @@ -5540,16 +6089,24 @@ jake@^10.8.5: jest-changed-files@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5" + resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz" integrity sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw== dependencies: "@jest/types" "^27.5.1" execa "^5.0.0" throat "^6.0.1" +jest-changed-files@^28.0.2: + version "28.0.2" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-28.0.2.tgz#7d7810660a5bd043af9e9cfbe4d58adb05e91531" + integrity sha512-QX9u+5I2s54ZnGoMEjiM2WeBvJR2J7w/8ZUmH2um/WLAuGAYFQcsVXY9+1YL6k0H/AGUdH8pXUAv6erDqEsvIA== + dependencies: + execa "^5.0.0" + throat "^6.0.1" + jest-circus@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.5.1.tgz#37a5a4459b7bf4406e53d637b49d22c65d125ecc" + resolved "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz" integrity sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw== dependencies: "@jest/environment" "^27.5.1" @@ -5572,9 +6129,34 @@ jest-circus@^27.5.1: stack-utils "^2.0.3" throat "^6.0.1" +jest-circus@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-28.1.0.tgz#e229f590911bd54d60efaf076f7acd9360296dae" + integrity sha512-rNYfqfLC0L0zQKRKsg4n4J+W1A2fbyGH7Ss/kDIocp9KXD9iaL111glsLu7+Z7FHuZxwzInMDXq+N1ZIBkI/TQ== + dependencies: + "@jest/environment" "^28.1.0" + "@jest/expect" "^28.1.0" + "@jest/test-result" "^28.1.0" + "@jest/types" "^28.1.0" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^0.7.0" + is-generator-fn "^2.0.0" + jest-each "^28.1.0" + jest-matcher-utils "^28.1.0" + jest-message-util "^28.1.0" + jest-runtime "^28.1.0" + jest-snapshot "^28.1.0" + jest-util "^28.1.0" + pretty-format "^28.1.0" + slash "^3.0.0" + stack-utils "^2.0.3" + throat "^6.0.1" + jest-cli@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.5.1.tgz#278794a6e6458ea8029547e6c6cbf673bd30b145" + resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz" integrity sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw== dependencies: "@jest/core" "^27.5.1" @@ -5590,9 +6172,27 @@ jest-cli@^27.5.1: prompts "^2.0.1" yargs "^16.2.0" +jest-cli@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-28.1.0.tgz#cd1d8adb9630102d5ba04a22895f63decdd7ac1f" + integrity sha512-fDJRt6WPRriHrBsvvgb93OxgajHHsJbk4jZxiPqmZbMDRcHskfJBBfTyjFko0jjfprP544hOktdSi9HVgl4VUQ== + dependencies: + "@jest/core" "^28.1.0" + "@jest/test-result" "^28.1.0" + "@jest/types" "^28.1.0" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + import-local "^3.0.2" + jest-config "^28.1.0" + jest-util "^28.1.0" + jest-validate "^28.1.0" + prompts "^2.0.1" + yargs "^17.3.1" + jest-config@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.5.1.tgz#5c387de33dca3f99ad6357ddeccd91bf3a0e4a41" + resolved "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz" integrity sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA== dependencies: "@babel/core" "^7.8.0" @@ -5620,9 +6220,37 @@ jest-config@^27.5.1: slash "^3.0.0" strip-json-comments "^3.1.1" +jest-config@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-28.1.0.tgz#fca22ca0760e746fe1ce1f9406f6b307ab818501" + integrity sha512-aOV80E9LeWrmflp7hfZNn/zGA4QKv/xsn2w8QCBP0t0+YqObuCWTSgNbHJ0j9YsTuCO08ZR/wsvlxqqHX20iUA== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^28.1.0" + "@jest/types" "^28.1.0" + babel-jest "^28.1.0" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^28.1.0" + jest-environment-node "^28.1.0" + jest-get-type "^28.0.2" + jest-regex-util "^28.0.2" + jest-resolve "^28.1.0" + jest-runner "^28.1.0" + jest-util "^28.1.0" + jest-validate "^28.1.0" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^28.1.0" + slash "^3.0.0" + strip-json-comments "^3.1.1" + jest-diff@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz" integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== dependencies: chalk "^4.0.0" @@ -5630,16 +6258,33 @@ jest-diff@^27.5.1: jest-get-type "^27.5.1" pretty-format "^27.5.1" +jest-diff@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-28.1.0.tgz#77686fef899ec1873dbfbf9330e37dd429703269" + integrity sha512-8eFd3U3OkIKRtlasXfiAQfbovgFgRDb0Ngcs2E+FMeBZ4rUezqIaGjuyggJBp+llosQXNEWofk/Sz4Hr5gMUhA== + dependencies: + chalk "^4.0.0" + diff-sequences "^28.0.2" + jest-get-type "^28.0.2" + pretty-format "^28.1.0" + jest-docblock@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.5.1.tgz#14092f364a42c6108d42c33c8cf30e058e25f6c0" + resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz" integrity sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ== dependencies: detect-newline "^3.0.0" +jest-docblock@^28.0.2: + version "28.0.2" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-28.0.2.tgz#3cab8abea53275c9d670cdca814fc89fba1298c2" + integrity sha512-FH10WWw5NxLoeSdQlJwu+MTiv60aXV/t8KEwIRGEv74WARE1cXIqh1vGdy2CraHuWOOrnzTWj/azQKqW4fO7xg== + dependencies: + detect-newline "^3.0.0" + jest-each@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.5.1.tgz#5bc87016f45ed9507fed6e4702a5b468a5b2c44e" + resolved "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz" integrity sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ== dependencies: "@jest/types" "^27.5.1" @@ -5648,9 +6293,20 @@ jest-each@^27.5.1: jest-util "^27.5.1" pretty-format "^27.5.1" +jest-each@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-28.1.0.tgz#54ae66d6a0a5b1913e9a87588d26c2687c39458b" + integrity sha512-a/XX02xF5NTspceMpHujmOexvJ4GftpYXqr6HhhmKmExtMXsyIN/fvanQlt/BcgFoRKN4OCXxLQKth9/n6OPFg== + dependencies: + "@jest/types" "^28.1.0" + chalk "^4.0.0" + jest-get-type "^28.0.2" + jest-util "^28.1.0" + pretty-format "^28.1.0" + jest-environment-jsdom@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz#ea9ccd1fc610209655a77898f86b2b559516a546" + resolved "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz" integrity sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw== dependencies: "@jest/environment" "^27.5.1" @@ -5663,7 +6319,7 @@ jest-environment-jsdom@^27.5.1: jest-environment-node@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.5.1.tgz#dedc2cfe52fab6b8f5714b4808aefa85357a365e" + resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz" integrity sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw== dependencies: "@jest/environment" "^27.5.1" @@ -5673,14 +6329,31 @@ jest-environment-node@^27.5.1: jest-mock "^27.5.1" jest-util "^27.5.1" +jest-environment-node@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-28.1.0.tgz#6ed2150aa31babba0c488c5b4f4d813a585c68e6" + integrity sha512-gBLZNiyrPw9CSMlTXF1yJhaBgWDPVvH0Pq6bOEwGMXaYNzhzhw2kA/OijNF8egbCgDS0/veRv97249x2CX+udQ== + dependencies: + "@jest/environment" "^28.1.0" + "@jest/fake-timers" "^28.1.0" + "@jest/types" "^28.1.0" + "@types/node" "*" + jest-mock "^28.1.0" + jest-util "^28.1.0" + jest-get-type@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz" integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== +jest-get-type@^28.0.2: + version "28.0.2" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-28.0.2.tgz#34622e628e4fdcd793d46db8a242227901fcf203" + integrity sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA== + jest-haste-map@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz" integrity sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng== dependencies: "@jest/types" "^27.5.1" @@ -5698,9 +6371,28 @@ jest-haste-map@^27.5.1: optionalDependencies: fsevents "^2.3.2" +jest-haste-map@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-28.1.0.tgz#6c1ee2daf1c20a3e03dbd8e5b35c4d73d2349cf0" + integrity sha512-xyZ9sXV8PtKi6NCrJlmq53PyNVHzxmcfXNVvIRHpHmh1j/HChC4pwKgyjj7Z9us19JMw8PpQTJsFWOsIfT93Dw== + dependencies: + "@jest/types" "^28.1.0" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^28.0.2" + jest-util "^28.1.0" + jest-worker "^28.1.0" + micromatch "^4.0.4" + walker "^1.0.7" + optionalDependencies: + fsevents "^2.3.2" + jest-jasmine2@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz#a037b0034ef49a9f3d71c4375a796f3b230d1ac4" + resolved "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz" integrity sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ== dependencies: "@jest/environment" "^27.5.1" @@ -5723,15 +6415,23 @@ jest-jasmine2@^27.5.1: jest-leak-detector@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz#6ec9d54c3579dd6e3e66d70e3498adf80fde3fb8" + resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz" integrity sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ== dependencies: jest-get-type "^27.5.1" pretty-format "^27.5.1" +jest-leak-detector@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-28.1.0.tgz#b65167776a8787443214d6f3f54935a4c73c8a45" + integrity sha512-uIJDQbxwEL2AMMs2xjhZl2hw8s77c3wrPaQ9v6tXJLGaaQ+4QrNJH5vuw7hA7w/uGT/iJ42a83opAqxGHeyRIA== + dependencies: + jest-get-type "^28.0.2" + pretty-format "^28.1.0" + jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz" integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== dependencies: chalk "^4.0.0" @@ -5739,9 +6439,19 @@ jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1: jest-get-type "^27.5.1" pretty-format "^27.5.1" +jest-matcher-utils@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-28.1.0.tgz#2ae398806668eeabd293c61712227cb94b250ccf" + integrity sha512-onnax0n2uTLRQFKAjC7TuaxibrPSvZgKTcSCnNUz/tOjJ9UhxNm7ZmPpoQavmTDUjXvUQ8KesWk2/VdrxIFzTQ== + dependencies: + chalk "^4.0.0" + jest-diff "^28.1.0" + jest-get-type "^28.0.2" + pretty-format "^28.1.0" + jest-message-util@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz" integrity sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g== dependencies: "@babel/code-frame" "^7.12.13" @@ -5754,36 +6464,72 @@ jest-message-util@^27.5.1: slash "^3.0.0" stack-utils "^2.0.3" +jest-message-util@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-28.1.0.tgz#7e8f0b9049e948e7b94c2a52731166774ba7d0af" + integrity sha512-RpA8mpaJ/B2HphDMiDlrAZdDytkmwFqgjDZovM21F35lHGeUeCvYmm6W+sbQ0ydaLpg5bFAUuWG1cjqOl8vqrw== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^28.1.0" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^28.1.0" + slash "^3.0.0" + stack-utils "^2.0.3" + jest-mock@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" + resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz" integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og== dependencies: "@jest/types" "^27.5.1" "@types/node" "*" +jest-mock@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-28.1.0.tgz#ccc7cc12a9b330b3182db0c651edc90d163ff73e" + integrity sha512-H7BrhggNn77WhdL7O1apG0Q/iwl0Bdd5E1ydhCJzL3oBLh/UYxAwR3EJLsBZ9XA3ZU4PA3UNw4tQjduBTCTmLw== + dependencies: + "@jest/types" "^28.1.0" + "@types/node" "*" + jest-pnp-resolver@^1.2.2: version "1.2.2" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" + resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== jest-regex-util@^27.0.0, jest-regex-util@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz" integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== +jest-regex-util@^28.0.2: + version "28.0.2" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-28.0.2.tgz#afdc377a3b25fb6e80825adcf76c854e5bf47ead" + integrity sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw== + jest-resolve-dependencies@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz#d811ecc8305e731cc86dd79741ee98fed06f1da8" + resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz" integrity sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg== dependencies: "@jest/types" "^27.5.1" jest-regex-util "^27.5.1" jest-snapshot "^27.5.1" +jest-resolve-dependencies@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.0.tgz#167becb8bee6e20b5ef4a3a728ec67aef6b0b79b" + integrity sha512-Ue1VYoSZquPwEvng7Uefw8RmZR+me/1kr30H2jMINjGeHgeO/JgrR6wxj2ofkJ7KSAA11W3cOrhNCbj5Dqqd9g== + dependencies: + jest-regex-util "^28.0.2" + jest-snapshot "^28.1.0" + jest-resolve@^27.4.2, jest-resolve@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.5.1.tgz#a2f1c5a0796ec18fe9eb1536ac3814c23617b384" + resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz" integrity sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw== dependencies: "@jest/types" "^27.5.1" @@ -5797,9 +6543,24 @@ jest-resolve@^27.4.2, jest-resolve@^27.5.1: resolve.exports "^1.1.0" slash "^3.0.0" +jest-resolve@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-28.1.0.tgz#b1f32748a6cee7d1779c7ef639c0a87078de3d35" + integrity sha512-vvfN7+tPNnnhDvISuzD1P+CRVP8cK0FHXRwPAcdDaQv4zgvwvag2n55/h5VjYcM5UJG7L4TwE5tZlzcI0X2Lhw== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^28.1.0" + jest-pnp-resolver "^1.2.2" + jest-util "^28.1.0" + jest-validate "^28.1.0" + resolve "^1.20.0" + resolve.exports "^1.1.0" + slash "^3.0.0" + jest-runner@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.5.1.tgz#071b27c1fa30d90540805c5645a0ec167c7b62e5" + resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz" integrity sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ== dependencies: "@jest/console" "^27.5.1" @@ -5824,9 +6585,36 @@ jest-runner@^27.5.1: source-map-support "^0.5.6" throat "^6.0.1" +jest-runner@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-28.1.0.tgz#aefe2a1e618a69baa0b24a50edc54fdd7e728eaa" + integrity sha512-FBpmuh1HB2dsLklAlRdOxNTTHKFR6G1Qmd80pVDvwbZXTriqjWqjei5DKFC1UlM732KjYcE6yuCdiF0WUCOS2w== + dependencies: + "@jest/console" "^28.1.0" + "@jest/environment" "^28.1.0" + "@jest/test-result" "^28.1.0" + "@jest/transform" "^28.1.0" + "@jest/types" "^28.1.0" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.10.2" + graceful-fs "^4.2.9" + jest-docblock "^28.0.2" + jest-environment-node "^28.1.0" + jest-haste-map "^28.1.0" + jest-leak-detector "^28.1.0" + jest-message-util "^28.1.0" + jest-resolve "^28.1.0" + jest-runtime "^28.1.0" + jest-util "^28.1.0" + jest-watcher "^28.1.0" + jest-worker "^28.1.0" + source-map-support "0.5.13" + throat "^6.0.1" + jest-runtime@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.5.1.tgz#4896003d7a334f7e8e4a53ba93fb9bcd3db0a1af" + resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz" integrity sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A== dependencies: "@jest/environment" "^27.5.1" @@ -5852,9 +6640,37 @@ jest-runtime@^27.5.1: slash "^3.0.0" strip-bom "^4.0.0" +jest-runtime@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-28.1.0.tgz#4847dcb2a4eb4b0f9eaf41306897e51fb1665631" + integrity sha512-wNYDiwhdH/TV3agaIyVF0lsJ33MhyujOe+lNTUiolqKt8pchy1Hq4+tDMGbtD5P/oNLA3zYrpx73T9dMTOCAcg== + dependencies: + "@jest/environment" "^28.1.0" + "@jest/fake-timers" "^28.1.0" + "@jest/globals" "^28.1.0" + "@jest/source-map" "^28.0.2" + "@jest/test-result" "^28.1.0" + "@jest/transform" "^28.1.0" + "@jest/types" "^28.1.0" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + execa "^5.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^28.1.0" + jest-message-util "^28.1.0" + jest-mock "^28.1.0" + jest-regex-util "^28.0.2" + jest-resolve "^28.1.0" + jest-snapshot "^28.1.0" + jest-util "^28.1.0" + slash "^3.0.0" + strip-bom "^4.0.0" + jest-serializer@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" + resolved "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz" integrity sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w== dependencies: "@types/node" "*" @@ -5862,7 +6678,7 @@ jest-serializer@^27.5.1: jest-snapshot@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.5.1.tgz#b668d50d23d38054a51b42c4039cab59ae6eb6a1" + resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz" integrity sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA== dependencies: "@babel/core" "^7.7.2" @@ -5888,9 +6704,38 @@ jest-snapshot@^27.5.1: pretty-format "^27.5.1" semver "^7.3.2" +jest-snapshot@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-28.1.0.tgz#4b74fa8816707dd10fe9d551c2c258e5a67b53b6" + integrity sha512-ex49M2ZrZsUyQLpLGxQtDbahvgBjlLPgklkqGM0hq/F7W/f8DyqZxVHjdy19QKBm4O93eDp+H5S23EiTbbUmHw== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/traverse" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^28.1.0" + "@jest/transform" "^28.1.0" + "@jest/types" "^28.1.0" + "@types/babel__traverse" "^7.0.6" + "@types/prettier" "^2.1.5" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^28.1.0" + graceful-fs "^4.2.9" + jest-diff "^28.1.0" + jest-get-type "^28.0.2" + jest-haste-map "^28.1.0" + jest-matcher-utils "^28.1.0" + jest-message-util "^28.1.0" + jest-util "^28.1.0" + natural-compare "^1.4.0" + pretty-format "^28.1.0" + semver "^7.3.5" + jest-util@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz" integrity sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw== dependencies: "@jest/types" "^27.5.1" @@ -5900,9 +6745,21 @@ jest-util@^27.5.1: graceful-fs "^4.2.9" picomatch "^2.2.3" +jest-util@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-28.1.0.tgz#d54eb83ad77e1dd441408738c5a5043642823be5" + integrity sha512-qYdCKD77k4Hwkose2YBEqQk7PzUf/NSE+rutzceduFveQREeH6b+89Dc9+wjX9dAwHcgdx4yedGA3FQlU/qCTA== + dependencies: + "@jest/types" "^28.1.0" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + jest-validate@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.5.1.tgz#9197d54dc0bdb52260b8db40b46ae668e04df067" + resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz" integrity sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ== dependencies: "@jest/types" "^27.5.1" @@ -5912,9 +6769,21 @@ jest-validate@^27.5.1: leven "^3.1.0" pretty-format "^27.5.1" +jest-validate@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-28.1.0.tgz#8a6821f48432aba9f830c26e28226ad77b9a0e18" + integrity sha512-Lly7CJYih3vQBfjLeANGgBSBJ7pEa18cxpQfQEq2go2xyEzehnHfQTjoUia8xUv4x4J80XKFIDwJJThXtRFQXQ== + dependencies: + "@jest/types" "^28.1.0" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^28.0.2" + leven "^3.1.0" + pretty-format "^28.1.0" + jest-watch-typeahead@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/jest-watch-typeahead/-/jest-watch-typeahead-1.0.0.tgz#4de2ca1eb596acb1889752afbab84b74fcd99173" + resolved "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-1.0.0.tgz" integrity sha512-jxoszalAb394WElmiJTFBMzie/RDCF+W7Q29n5LzOPtcoQoHWfdUtHFkbhgf5NwWe8uMOxvKb/g7ea7CshfkTw== dependencies: ansi-escapes "^4.3.1" @@ -5927,7 +6796,7 @@ jest-watch-typeahead@^1.0.0: jest-watcher@^27.0.0, jest-watcher@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.5.1.tgz#71bd85fb9bde3a2c2ec4dc353437971c43c642a2" + resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz" integrity sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw== dependencies: "@jest/test-result" "^27.5.1" @@ -5938,9 +6807,23 @@ jest-watcher@^27.0.0, jest-watcher@^27.5.1: jest-util "^27.5.1" string-length "^4.0.1" +jest-watcher@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-28.1.0.tgz#aaa7b4164a4e77eeb5f7d7b25ede5e7b4e9c9aaf" + integrity sha512-tNHMtfLE8Njcr2IRS+5rXYA4BhU90gAOwI9frTGOqd+jX0P/Au/JfRSNqsf5nUTcWdbVYuLxS1KjnzILSoR5hA== + dependencies: + "@jest/test-result" "^28.1.0" + "@jest/types" "^28.1.0" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.10.2" + jest-util "^28.1.0" + string-length "^4.0.1" + jest-worker@^26.2.1: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz" integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== dependencies: "@types/node" "*" @@ -5949,30 +6832,48 @@ jest-worker@^26.2.1: jest-worker@^27.0.2, jest-worker@^27.3.1, jest-worker@^27.4.5, jest-worker@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz" integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== dependencies: "@types/node" "*" merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^27.4.3, jest@^27.5.1: +jest-worker@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-28.1.0.tgz#ced54757a035e87591e1208253a6e3aac1a855e5" + integrity sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^27.4.3: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest/-/jest-27.5.1.tgz#dadf33ba70a779be7a6fc33015843b51494f63fc" + resolved "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz" integrity sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ== dependencies: "@jest/core" "^27.5.1" import-local "^3.0.2" jest-cli "^27.5.1" +jest@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-28.1.0.tgz#f420e41c8f2395b9a30445a97189ebb57593d831" + integrity sha512-TZR+tHxopPhzw3c3560IJXZWLNHgpcz1Zh0w5A65vynLGNcg/5pZ+VildAd7+XGOu6jd58XMY/HNn0IkZIXVXg== + dependencies: + "@jest/core" "^28.1.0" + import-local "^3.0.2" + jest-cli "^28.1.0" + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.1: version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== dependencies: argparse "^1.0.7" @@ -5980,14 +6881,14 @@ js-yaml@^3.13.1: js-yaml@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: argparse "^2.0.1" jsdom@^16.6.0: version "16.7.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" + resolved "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz" integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== dependencies: abab "^2.0.5" @@ -6020,66 +6921,71 @@ jsdom@^16.6.0: jsesc@^2.5.1: version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== jsesc@~0.5.0: version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz" integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= json-parse-better-errors@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== json-parse-even-better-errors@^2.3.0: version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== json-schema-traverse@^0.4.1: version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== json-schema-traverse@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== json-schema@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz" integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= json5@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + resolved "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz" integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== dependencies: minimist "^1.2.0" json5@^2.1.2, json5@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz" integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== dependencies: minimist "^1.2.5" +json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + jsonc-parser@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" + resolved "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz" integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== jsonfile@^6.0.1: version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== dependencies: universalify "^2.0.0" @@ -6088,12 +6994,12 @@ jsonfile@^6.0.1: jsonpointer@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.0.tgz#f802669a524ec4805fa7389eadbc9921d5dc8072" + resolved "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.0.tgz" integrity sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg== "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1: version "3.2.1" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b" + resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz" integrity sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA== dependencies: array-includes "^3.1.3" @@ -6101,39 +7007,39 @@ jsonpointer@^5.0.0: kind-of@^6.0.2: version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== kleur@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== klona@^2.0.4, klona@^2.0.5: version "2.0.5" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc" + resolved "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz" integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== language-subtag-registry@~0.3.2: version "0.3.21" - resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a" + resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz" integrity sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg== language-tags@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a" + resolved "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz" integrity sha1-0yHbxNowuovzAk4ED6XBRmH5GTo= dependencies: language-subtag-registry "~0.3.2" leven@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== levn@^0.4.1: version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== dependencies: prelude-ls "^1.2.1" @@ -6141,7 +7047,7 @@ levn@^0.4.1: levn@~0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + resolved "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz" integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= dependencies: prelude-ls "~1.1.2" @@ -6149,22 +7055,22 @@ levn@~0.3.0: lilconfig@^2.0.3, lilconfig@^2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz" integrity sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA== lines-and-columns@^1.1.6: version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== loader-runner@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" + resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz" integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== loader-utils@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" + resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz" integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== dependencies: big.js "^5.2.2" @@ -6173,7 +7079,7 @@ loader-utils@^1.4.0: loader-utils@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129" + resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz" integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A== dependencies: big.js "^5.2.2" @@ -6182,12 +7088,12 @@ loader-utils@^2.0.0: loader-utils@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.0.tgz#bcecc51a7898bee7473d4bc6b845b23af8304d4f" + resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz" integrity sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ== locate-path@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz" integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= dependencies: p-locate "^2.0.0" @@ -6195,7 +7101,7 @@ locate-path@^2.0.0: locate-path@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz" integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== dependencies: p-locate "^3.0.0" @@ -6203,165 +7109,170 @@ locate-path@^3.0.0: locate-path@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== dependencies: p-locate "^4.1.0" locate-path@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== dependencies: p-locate "^5.0.0" lodash.debounce@^4.0.8: version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= lodash.escape@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" + resolved "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz" integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg= lodash.flattendeep@^4.4.0: version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" + resolved "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz" integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= lodash.isequal@^4.5.0: version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= lodash.memoize@^4.1.2: version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= lodash.merge@^4.6.2: version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== lodash.sortby@^4.7.0: version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= lodash.uniq@^4.5.0: version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== dependencies: js-tokens "^3.0.0 || ^4.0.0" lower-case@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + resolved "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz" integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== dependencies: tslib "^2.0.3" lru-cache@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== dependencies: yallist "^4.0.0" lunr@^2.3.9: version "2.3.9" - resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" + resolved "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz" integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== lz-string@^1.4.4: version "1.4.4" - resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz" integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.9" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz" integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== dependencies: sourcemap-codec "^1.4.8" make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== dependencies: semver "^6.0.0" makeerror@1.0.12: version "1.0.12" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + resolved "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz" integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== dependencies: tmpl "1.0.5" marked@^4.0.12: version "4.0.12" - resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.12.tgz#2262a4e6fd1afd2f13557726238b69a48b982f7d" + resolved "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz" integrity sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ== mdn-data@2.0.14: version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz" integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== mdn-data@2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" + resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz" integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== media-typer@0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= memfs@^3.1.2, memfs@^3.4.1: version "3.4.1" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.1.tgz#b78092f466a0dce054d63d39275b24c71d3f1305" + resolved "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz" integrity sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw== dependencies: fs-monkey "1.0.3" +memoize-one@^5.0.0, memoize-one@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + merge-descriptors@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= merge-stream@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== methods@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= micromatch@^4.0.2, micromatch@^4.0.4: version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz" integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== dependencies: braces "^3.0.1" @@ -6369,122 +7280,127 @@ micromatch@^4.0.2, micromatch@^4.0.4: mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" mime@1.6.0: version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== mimic-fn@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== min-indent@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== mini-css-extract-plugin@^2.4.5: version "2.6.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.0.tgz#578aebc7fc14d32c0ad304c2c34f08af44673f5e" + resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.0.tgz" integrity sha512-ndG8nxCEnAemsg4FSgS+yNyHKgkTB4nPKqCOgh65j3/30qqC5RaSQQXMm++Y6sb6E1zRSxPkztj9fqxhS1Eo6w== dependencies: schema-utils "^4.0.0" minimalistic-assert@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== minimatch@3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" minimatch@^3.0.4, minimatch@^3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" minimatch@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz" integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== dependencies: brace-expansion "^2.0.1" minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== mkdirp@^0.5.5, mkdirp@~0.5.1: version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== dependencies: minimist "^1.2.5" moo@^0.5.0: version "0.5.1" - resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4" + resolved "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz" integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w== ms@2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= ms@2.1.2: version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== ms@2.1.3, ms@^2.1.1: version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== multicast-dns-service-types@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + resolved "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz" integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= multicast-dns@^6.0.1: version "6.2.3" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" + resolved "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz" integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== dependencies: dns-packet "^1.3.1" thunky "^1.0.2" +multiselect-react-dropdown@^2.0.22: + version "2.0.22" + resolved "https://registry.yarnpkg.com/multiselect-react-dropdown/-/multiselect-react-dropdown-2.0.22.tgz#9eddcb75ff74e29ca0c4ae1a4b40c538b7b24c46" + integrity sha512-Oc6Y/Rl1U3XSyp2Hb5utTvqtHeFTMJf5+PHrmxvk06yT02KLivqXUhmqsmDnLfMZm3UC6Hnh4NnpoNxj/z+5gg== + nanoid@^3.3.1: version "3.3.1" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz" integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== natural-compare@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= nearley@^2.7.10: version "2.20.1" - resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474" + resolved "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz" integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ== dependencies: commander "^2.19.0" @@ -6494,17 +7410,17 @@ nearley@^2.7.10: negotiator@0.6.3: version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== neo-async@^2.6.2: version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== no-case@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz" integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== dependencies: lower-case "^2.0.2" @@ -6512,78 +7428,78 @@ no-case@^3.0.4: node-forge@^1.2.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.0.tgz#37a874ea723855f37db091e6c186e5b67a01d4b2" + resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz" integrity sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA== node-int64@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= node-releases@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz" integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== normalize-range@^0.1.2: version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= normalize-url@^6.0.1: version "6.1.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== npm-run-path@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== dependencies: path-key "^3.0.0" nth-check@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + resolved "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz" integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== dependencies: boolbase "~1.0.0" nth-check@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" + resolved "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz" integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== dependencies: boolbase "^1.0.0" nwsapi@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" + resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== object-assign@^4.1.1: version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= object-hash@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + resolved "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz" integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== object-inspect@^1.11.0, object-inspect@^1.7.0, object-inspect@^1.9.0: version "1.12.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== object-is@^1.0.1, object-is@^1.0.2, object-is@^1.1.2: version "1.1.5" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" + resolved "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz" integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== dependencies: call-bind "^1.0.2" @@ -6591,12 +7507,12 @@ object-is@^1.0.1, object-is@^1.0.2, object-is@^1.1.2: object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== object.assign@^4.1.0, object.assign@^4.1.2: version "4.1.2" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz" integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== dependencies: call-bind "^1.0.0" @@ -6606,7 +7522,7 @@ object.assign@^4.1.0, object.assign@^4.1.2: object.entries@^1.1.1, object.entries@^1.1.2, object.entries@^1.1.5: version "1.1.5" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" + resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz" integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== dependencies: call-bind "^1.0.2" @@ -6615,7 +7531,7 @@ object.entries@^1.1.1, object.entries@^1.1.2, object.entries@^1.1.5: object.fromentries@^2.0.3, object.fromentries@^2.0.5: version "2.0.5" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.5.tgz#7b37b205109c21e741e605727fe8b0ad5fa08251" + resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz" integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw== dependencies: call-bind "^1.0.2" @@ -6624,7 +7540,7 @@ object.fromentries@^2.0.3, object.fromentries@^2.0.5: object.getownpropertydescriptors@^2.1.0: version "2.1.3" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz#b223cf38e17fefb97a63c10c91df72ccb386df9e" + resolved "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz" integrity sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw== dependencies: call-bind "^1.0.2" @@ -6633,7 +7549,7 @@ object.getownpropertydescriptors@^2.1.0: object.hasown@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.0.tgz#7232ed266f34d197d15cac5880232f7a4790afe5" + resolved "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.0.tgz" integrity sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg== dependencies: define-properties "^1.1.3" @@ -6641,7 +7557,7 @@ object.hasown@^1.1.0: object.values@^1.1.0, object.values@^1.1.1, object.values@^1.1.2, object.values@^1.1.5: version "1.1.5" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" + resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz" integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== dependencies: call-bind "^1.0.2" @@ -6650,38 +7566,38 @@ object.values@^1.1.0, object.values@^1.1.1, object.values@^1.1.2, object.values@ obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== on-finished@~2.3.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= dependencies: ee-first "1.1.1" on-headers@~1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== once@^1.3.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" onetime@^5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" open@^8.0.9, open@^8.4.0: version "8.4.0" - resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" + resolved "https://registry.npmjs.org/open/-/open-8.4.0.tgz" integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q== dependencies: define-lazy-prop "^2.0.0" @@ -6690,7 +7606,7 @@ open@^8.0.9, open@^8.4.0: optionator@^0.8.1: version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz" integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== dependencies: deep-is "~0.1.3" @@ -6702,7 +7618,7 @@ optionator@^0.8.1: optionator@^0.9.1: version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== dependencies: deep-is "^0.1.3" @@ -6714,63 +7630,63 @@ optionator@^0.9.1: p-limit@^1.1.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz" integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== dependencies: p-try "^1.0.0" p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== dependencies: p-try "^2.0.0" p-limit@^3.0.2: version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: yocto-queue "^0.1.0" p-locate@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz" integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= dependencies: p-limit "^1.1.0" p-locate@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz" integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== dependencies: p-limit "^2.0.0" p-locate@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== dependencies: p-limit "^2.2.0" p-locate@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== dependencies: p-limit "^3.0.2" p-map@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz" integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== dependencies: aggregate-error "^3.0.0" p-retry@^4.5.0: version "4.6.1" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.1.tgz#8fcddd5cdf7a67a0911a9cf2ef0e5df7f602316c" + resolved "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz" integrity sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA== dependencies: "@types/retry" "^0.12.0" @@ -6778,17 +7694,17 @@ p-retry@^4.5.0: p-try@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz" integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= p-try@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== param-case@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" + resolved "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz" integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== dependencies: dot-case "^3.0.4" @@ -6796,14 +7712,14 @@ param-case@^3.0.4: parent-module@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== dependencies: callsites "^3.0.0" parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== dependencies: "@babel/code-frame" "^7.0.0" @@ -6813,24 +7729,24 @@ parse-json@^5.0.0, parse-json@^5.2.0: parse5-htmlparser2-tree-adapter@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" + resolved "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz" integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== dependencies: parse5 "^6.0.1" parse5@6.0.1, parse5@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + resolved "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== pascal-case@^3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + resolved "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz" integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== dependencies: no-case "^3.0.4" @@ -6838,81 +7754,81 @@ pascal-case@^3.1.2: path-exists@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz" integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= path-exists@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== path-is-absolute@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.6, path-parse@^1.0.7: version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-to-regexp@0.1.7: version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= path-type@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== performance-now@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= picocolors@^0.2.1: version "0.2.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz" integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== picocolors@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.0: version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== pirates@^4.0.4: version "4.0.5" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz" integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== dependencies: find-up "^4.0.0" pkg-up@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" + resolved "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz" integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== dependencies: find-up "^3.0.0" portfinder@^1.0.28: version "1.0.28" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" + resolved "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz" integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== dependencies: async "^2.6.2" @@ -6921,19 +7837,19 @@ portfinder@^1.0.28: postcss-attribute-case-insensitive@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.0.tgz#39cbf6babf3ded1e4abf37d09d6eda21c644105c" + resolved "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.0.tgz" integrity sha512-b4g9eagFGq9T5SWX4+USfVyjIb3liPnjhHHRMP7FMB2kFVpYyfEscV0wP3eaXhKlcHKUut8lt5BGoeylWA/dBQ== dependencies: postcss-selector-parser "^6.0.2" postcss-browser-comments@^4: version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz#bcfc86134df5807f5d3c0eefa191d42136b5e72a" + resolved "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz" integrity sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg== postcss-calc@^8.2.3: version "8.2.4" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" + resolved "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz" integrity sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q== dependencies: postcss-selector-parser "^6.0.9" @@ -6941,35 +7857,35 @@ postcss-calc@^8.2.3: postcss-clamp@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-clamp/-/postcss-clamp-4.1.0.tgz#7263e95abadd8c2ba1bd911b0b5a5c9c93e02363" + resolved "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz" integrity sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow== dependencies: postcss-value-parser "^4.2.0" postcss-color-functional-notation@^4.2.2: version "4.2.2" - resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.2.tgz#f59ccaeb4ee78f1b32987d43df146109cc743073" + resolved "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.2.tgz" integrity sha512-DXVtwUhIk4f49KK5EGuEdgx4Gnyj6+t2jBSEmxvpIK9QI40tWrpS2Pua8Q7iIZWBrki2QOaeUdEaLPPa91K0RQ== dependencies: postcss-value-parser "^4.2.0" postcss-color-hex-alpha@^8.0.3: version "8.0.3" - resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.3.tgz#61a0fd151d28b128aa6a8a21a2dad24eebb34d52" + resolved "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.3.tgz" integrity sha512-fESawWJCrBV035DcbKRPAVmy21LpoyiXdPTuHUfWJ14ZRjY7Y7PA6P4g8z6LQGYhU1WAxkTxjIjurXzoe68Glw== dependencies: postcss-value-parser "^4.2.0" postcss-color-rebeccapurple@^7.0.2: version "7.0.2" - resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.0.2.tgz#5d397039424a58a9ca628762eb0b88a61a66e079" + resolved "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.0.2.tgz" integrity sha512-SFc3MaocHaQ6k3oZaFwH8io6MdypkUtEy/eXzXEB1vEQlO3S3oDc/FSZA8AsS04Z25RirQhlDlHLh3dn7XewWw== dependencies: postcss-value-parser "^4.2.0" postcss-colormin@^*: version "5.3.0" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.0.tgz#3cee9e5ca62b2c27e84fce63affc0cfb5901956a" + resolved "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.0.tgz" integrity sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg== dependencies: browserslist "^4.16.6" @@ -6979,60 +7895,60 @@ postcss-colormin@^*: postcss-convert-values@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.1.0.tgz#f8d3abe40b4ce4b1470702a0706343eac17e7c10" + resolved "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.0.tgz" integrity sha512-GkyPbZEYJiWtQB0KZ0X6qusqFHUepguBCNFi9t5JJc7I2OTXG7C0twbTLvCfaKOLl3rSXmpAwV7W5txd91V84g== dependencies: postcss-value-parser "^4.2.0" postcss-custom-media@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-8.0.0.tgz#1be6aff8be7dc9bf1fe014bde3b71b92bb4552f1" + resolved "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.0.tgz" integrity sha512-FvO2GzMUaTN0t1fBULDeIvxr5IvbDXcIatt6pnJghc736nqNgsGao5NT+5+WVLAQiTt6Cb3YUms0jiPaXhL//g== postcss-custom-properties@^12.1.5: version "12.1.5" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-12.1.5.tgz#e669cfff89b0ea6fc85c45864a32b450cb6b196f" + resolved "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.5.tgz" integrity sha512-FHbbB/hRo/7cxLGkc2NS7cDRIDN1oFqQnUKBiyh4b/gwk8DD8udvmRDpUhEK836kB8ggUCieHVOvZDnF9XhI3g== dependencies: postcss-value-parser "^4.2.0" postcss-custom-selectors@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-6.0.0.tgz#022839e41fbf71c47ae6e316cb0e6213012df5ef" + resolved "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.0.tgz" integrity sha512-/1iyBhz/W8jUepjGyu7V1OPcGbc636snN1yXEQCinb6Bwt7KxsiU7/bLQlp8GwAXzCh7cobBU5odNn/2zQWR8Q== dependencies: postcss-selector-parser "^6.0.4" postcss-dir-pseudo-class@^6.0.4: version "6.0.4" - resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.4.tgz#9afe49ea631f0cb36fa0076e7c2feb4e7e3f049c" + resolved "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.4.tgz" integrity sha512-I8epwGy5ftdzNWEYok9VjW9whC4xnelAtbajGv4adql4FIF09rnrxnA9Y8xSHN47y7gqFIv10C5+ImsLeJpKBw== dependencies: postcss-selector-parser "^6.0.9" postcss-discard-comments@^*: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.1.1.tgz#e90019e1a0e5b99de05f63516ce640bd0df3d369" + resolved "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.1.tgz" integrity sha512-5JscyFmvkUxz/5/+TB3QTTT9Gi9jHkcn8dcmmuN68JQcv3aQg4y88yEHHhwFB52l/NkaJ43O0dbksGMAo49nfQ== postcss-discard-duplicates@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848" + resolved "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz" integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== postcss-discard-empty@^*: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz#e57762343ff7f503fe53fca553d18d7f0c369c6c" + resolved "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz" integrity sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A== postcss-discard-overridden@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz#7e8c5b53325747e9d90131bb88635282fb4a276e" + resolved "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz" integrity sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw== postcss-double-position-gradients@^3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.1.tgz#a12cfdb7d11fa1a99ccecc747f0c19718fb37152" + resolved "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.1.tgz" integrity sha512-jM+CGkTs4FcG53sMPjrrGE0rIvLDdCrqMzgDC5fLI7JHDO7o6QG8C5TQBtExb13hdBdoH9C2QVbG4jo2y9lErQ== dependencies: "@csstools/postcss-progressive-custom-properties" "^1.1.0" @@ -7040,62 +7956,62 @@ postcss-double-position-gradients@^3.1.1: postcss-env-function@^4.0.6: version "4.0.6" - resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-4.0.6.tgz#7b2d24c812f540ed6eda4c81f6090416722a8e7a" + resolved "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz" integrity sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA== dependencies: postcss-value-parser "^4.2.0" postcss-flexbugs-fixes@^5.0.2: version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz#2028e145313074fc9abe276cb7ca14e5401eb49d" + resolved "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz" integrity sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ== postcss-focus-visible@^6.0.4: version "6.0.4" - resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz#50c9ea9afa0ee657fb75635fabad25e18d76bf9e" + resolved "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz" integrity sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw== dependencies: postcss-selector-parser "^6.0.9" postcss-focus-within@^5.0.4: version "5.0.4" - resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz#5b1d2ec603195f3344b716c0b75f61e44e8d2e20" + resolved "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz" integrity sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ== dependencies: postcss-selector-parser "^6.0.9" postcss-font-variant@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz#efd59b4b7ea8bb06127f2d031bfbb7f24d32fa66" + resolved "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz" integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== postcss-gap-properties@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-3.0.3.tgz#6401bb2f67d9cf255d677042928a70a915e6ba60" + resolved "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.3.tgz" integrity sha512-rPPZRLPmEKgLk/KlXMqRaNkYTUpE7YC+bOIQFN5xcu1Vp11Y4faIXv6/Jpft6FMnl6YRxZqDZG0qQOW80stzxQ== postcss-image-set-function@^4.0.6: version "4.0.6" - resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-4.0.6.tgz#bcff2794efae778c09441498f40e0c77374870a9" + resolved "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.6.tgz" integrity sha512-KfdC6vg53GC+vPd2+HYzsZ6obmPqOk6HY09kttU19+Gj1nC3S3XBVEXDHxkhxTohgZqzbUb94bKXvKDnYWBm/A== dependencies: postcss-value-parser "^4.2.0" postcss-initial@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-4.0.1.tgz#529f735f72c5724a0fb30527df6fb7ac54d7de42" + resolved "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz" integrity sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ== postcss-js@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.0.tgz#31db79889531b80dc7bc9b0ad283e418dce0ac00" + resolved "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz" integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ== dependencies: camelcase-css "^2.0.1" postcss-lab-function@^4.1.2: version "4.1.2" - resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-4.1.2.tgz#b75afe43ba9c1f16bfe9bb12c8109cabd55b5fc2" + resolved "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.1.2.tgz" integrity sha512-isudf5ldhg4fk16M8viAwAbg6Gv14lVO35N3Z/49NhbwPQ2xbiEoHgrRgpgQojosF4vF7jY653ktB6dDrUOR8Q== dependencies: "@csstools/postcss-progressive-custom-properties" "^1.1.0" @@ -7103,7 +8019,7 @@ postcss-lab-function@^4.1.2: postcss-load-config@^3.1.0: version "3.1.3" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.3.tgz#21935b2c43b9a86e6581a576ca7ee1bde2bd1d23" + resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.3.tgz" integrity sha512-5EYgaM9auHGtO//ljHH+v/aC/TQ5LHXtL7bQajNAUBKUVKiYE8rYpFms7+V26D9FncaGe2zwCoPQsFKb5zF/Hw== dependencies: lilconfig "^2.0.4" @@ -7111,7 +8027,7 @@ postcss-load-config@^3.1.0: postcss-loader@^6.2.1: version "6.2.1" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-6.2.1.tgz#0895f7346b1702103d30fdc66e4d494a93c008ef" + resolved "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz" integrity sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q== dependencies: cosmiconfig "^7.0.0" @@ -7120,17 +8036,17 @@ postcss-loader@^6.2.1: postcss-logical@^5.0.4: version "5.0.4" - resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-5.0.4.tgz#ec75b1ee54421acc04d5921576b7d8db6b0e6f73" + resolved "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz" integrity sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g== postcss-media-minmax@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz#7140bddec173e2d6d657edbd8554a55794e2a5b5" + resolved "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz" integrity sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ== postcss-merge-longhand@^*: version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.2.tgz#fe3002f38ad5827c1d6f7d5bb3f71d2566a2a138" + resolved "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.2.tgz" integrity sha512-18/bp9DZnY1ai9RlahOfLBbmIUKfKFPASxRCiZ1vlpZqWPCn8qWPFlEozqmWL+kBtcEQmG8W9YqGCstDImvp/Q== dependencies: postcss-value-parser "^4.2.0" @@ -7138,7 +8054,7 @@ postcss-merge-longhand@^*: postcss-merge-rules@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.0.tgz#a2d5117eba09c8686a5471d97bd9afcf30d1b41f" + resolved "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.0.tgz" integrity sha512-NecukEJovQ0mG7h7xV8wbYAkXGTO3MPKnXvuiXzOKcxoOodfTTKYjeo8TMhAswlSkjcPIBlnKbSFcTuVSDaPyQ== dependencies: browserslist "^4.16.6" @@ -7148,14 +8064,14 @@ postcss-merge-rules@^*: postcss-minify-font-values@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz#f1df0014a726083d260d3bd85d7385fb89d1f01b" + resolved "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz" integrity sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA== dependencies: postcss-value-parser "^4.2.0" postcss-minify-gradients@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.1.0.tgz#de0260a67a13b7b321a8adc3150725f2c6612377" + resolved "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.0.tgz" integrity sha512-J/TMLklkONn3LuL8wCwfwU8zKC1hpS6VcxFkNUNjmVt53uKqrrykR3ov11mdUYyqVMEx67slMce0tE14cE4DTg== dependencies: colord "^2.9.1" @@ -7164,7 +8080,7 @@ postcss-minify-gradients@^*: postcss-minify-params@^*: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.1.1.tgz#c5f8e7dac565e577dd99904787fbec576cbdbfb2" + resolved "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.1.tgz" integrity sha512-WCpr+J9Uz8XzMpAfg3UL8z5rde6MifBbh5L8bn8S2F5hq/YDJJzASYCnCHvAB4Fqb94ys8v95ULQkW2EhCFvNg== dependencies: browserslist "^4.16.6" @@ -7173,19 +8089,19 @@ postcss-minify-params@^*: postcss-minify-selectors@^*: version "5.2.0" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.0.tgz#17c2be233e12b28ffa8a421a02fc8b839825536c" + resolved "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.0.tgz" integrity sha512-vYxvHkW+iULstA+ctVNx0VoRAR4THQQRkG77o0oa4/mBS0OzGvvzLIvHDv/nNEM0crzN2WIyFU5X7wZhaUK3RA== dependencies: postcss-selector-parser "^6.0.5" postcss-modules-extract-imports@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" + resolved "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz" integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== postcss-modules-local-by-default@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" + resolved "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz" integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== dependencies: icss-utils "^5.0.0" @@ -7194,75 +8110,75 @@ postcss-modules-local-by-default@^4.0.0: postcss-modules-scope@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" + resolved "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz" integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== dependencies: postcss-selector-parser "^6.0.4" postcss-modules-values@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + resolved "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz" integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== dependencies: icss-utils "^5.0.0" postcss-nested@5.0.6: version "5.0.6" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-5.0.6.tgz#466343f7fc8d3d46af3e7dba3fcd47d052a945bc" + resolved "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz" integrity sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA== dependencies: postcss-selector-parser "^6.0.6" postcss-nesting@^10.1.3: version "10.1.3" - resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-10.1.3.tgz#f0b1cd7ae675c697ab6a5a5ca1feea4784a2ef77" + resolved "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.1.3.tgz" integrity sha512-wUC+/YCik4wH3StsbC5fBG1s2Z3ZV74vjGqBFYtmYKlVxoio5TYGM06AiaKkQPPlkXWn72HKfS7Cw5PYxnoXSw== dependencies: postcss-selector-parser "^6.0.9" postcss-normalize-charset@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz#9302de0b29094b52c259e9b2cf8dc0879879f0ed" + resolved "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz" integrity sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg== postcss-normalize-display-values@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz#72abbae58081960e9edd7200fcf21ab8325c3da8" + resolved "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz" integrity sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA== dependencies: postcss-value-parser "^4.2.0" postcss-normalize-positions@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.0.tgz#902a7cb97cf0b9e8b1b654d4a43d451e48966458" + resolved "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.0.tgz" integrity sha512-8gmItgA4H5xiUxgN/3TVvXRoJxkAWLW6f/KKhdsH03atg0cB8ilXnrB5PpSshwVu/dD2ZsRFQcR1OEmSBDAgcQ== dependencies: postcss-value-parser "^4.2.0" postcss-normalize-repeat-style@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.0.tgz#f6d6fd5a54f51a741cc84a37f7459e60ef7a6398" + resolved "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.0.tgz" integrity sha512-IR3uBjc+7mcWGL6CtniKNQ4Rr5fTxwkaDHwMBDGGs1x9IVRkYIT/M4NelZWkAOBdV6v3Z9S46zqaKGlyzHSchw== dependencies: postcss-value-parser "^4.2.0" postcss-normalize-string@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz#411961169e07308c82c1f8c55f3e8a337757e228" + resolved "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz" integrity sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w== dependencies: postcss-value-parser "^4.2.0" postcss-normalize-timing-functions@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz#d5614410f8f0b2388e9f240aa6011ba6f52dafbb" + resolved "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz" integrity sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg== dependencies: postcss-value-parser "^4.2.0" postcss-normalize-unicode@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.0.tgz#3d23aede35e160089a285e27bf715de11dc9db75" + resolved "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.0.tgz" integrity sha512-J6M3MizAAZ2dOdSjy2caayJLQT8E8K9XjLce8AUQMwOrCvjCHv24aLC/Lps1R1ylOfol5VIDMaM/Lo9NGlk1SQ== dependencies: browserslist "^4.16.6" @@ -7270,7 +8186,7 @@ postcss-normalize-unicode@^*: postcss-normalize-url@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz#ed9d88ca82e21abef99f743457d3729a042adcdc" + resolved "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz" integrity sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew== dependencies: normalize-url "^6.0.1" @@ -7278,14 +8194,14 @@ postcss-normalize-url@^*: postcss-normalize-whitespace@^*: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz#08a1a0d1ffa17a7cc6efe1e6c9da969cc4493cfa" + resolved "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz" integrity sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA== dependencies: postcss-value-parser "^4.2.0" postcss-normalize@^10.0.1: version "10.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize/-/postcss-normalize-10.0.1.tgz#464692676b52792a06b06880a176279216540dd7" + resolved "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz" integrity sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA== dependencies: "@csstools/normalize.css" "*" @@ -7294,12 +8210,12 @@ postcss-normalize@^10.0.1: postcss-opacity-percentage@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.2.tgz#bd698bb3670a0a27f6d657cc16744b3ebf3b1145" + resolved "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.2.tgz" integrity sha512-lyUfF7miG+yewZ8EAk9XUBIlrHyUE6fijnesuz+Mj5zrIHIEw6KcIZSOk/elVMqzLvREmXB83Zi/5QpNRYd47w== postcss-ordered-values@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.1.0.tgz#04ef429e0991b0292bc918b135cd4c038f7b889f" + resolved "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.0.tgz" integrity sha512-wU4Z4D4uOIH+BUKkYid36gGDJNQtkVJT7Twv8qH6UyfttbbJWyw4/xIPuVEkkCtQLAJ0EdsNSh8dlvqkXb49TA== dependencies: cssnano-utils "^3.1.0" @@ -7307,24 +8223,24 @@ postcss-ordered-values@^*: postcss-overflow-shorthand@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.3.tgz#ebcfc0483a15bbf1b27fdd9b3c10125372f4cbc2" + resolved "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.3.tgz" integrity sha512-CxZwoWup9KXzQeeIxtgOciQ00tDtnylYIlJBBODqkgS/PU2jISuWOL/mYLHmZb9ZhZiCaNKsCRiLp22dZUtNsg== postcss-page-break@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-3.0.4.tgz#7fbf741c233621622b68d435babfb70dd8c1ee5f" + resolved "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz" integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ== postcss-place@^7.0.4: version "7.0.4" - resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-7.0.4.tgz#eb026650b7f769ae57ca4f938c1addd6be2f62c9" + resolved "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.4.tgz" integrity sha512-MrgKeiiu5OC/TETQO45kV3npRjOFxEHthsqGtkh3I1rPbZSbXGD/lZVi9j13cYh+NA8PIAPyk6sGjT9QbRyvSg== dependencies: postcss-value-parser "^4.2.0" postcss-preset-env@^7.0.1: version "7.4.3" - resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-7.4.3.tgz#fb1c8b4cb405da042da0ddb8c5eda7842c08a449" + resolved "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.4.3.tgz" integrity sha512-dlPA65g9KuGv7YsmGyCKtFkZKCPLkoVMUE3omOl6yM+qrynVHxFvf0tMuippIrXB/sB/MyhL1FgTIbrO+qMERg== dependencies: "@csstools/postcss-color-function" "^1.0.3" @@ -7373,14 +8289,14 @@ postcss-preset-env@^7.0.1: postcss-pseudo-class-any-link@^7.1.1: version "7.1.1" - resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.1.tgz#534eb1dadd9945eb07830dbcc06fb4d5d865b8e0" + resolved "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.1.tgz" integrity sha512-JRoLFvPEX/1YTPxRxp1JO4WxBVXJYrSY7NHeak5LImwJ+VobFMwYDQHvfTXEpcn+7fYIeGkC29zYFhFWIZD8fg== dependencies: postcss-selector-parser "^6.0.9" postcss-reduce-initial@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.0.tgz#fc31659ea6e85c492fb2a7b545370c215822c5d6" + resolved "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.0.tgz" integrity sha512-5OgTUviz0aeH6MtBjHfbr57tml13PuedK/Ecg8szzd4XRMbYxH4572JFG067z+FqBIf6Zp/d+0581glkvvWMFw== dependencies: browserslist "^4.16.6" @@ -7388,26 +8304,26 @@ postcss-reduce-initial@^*: postcss-reduce-transforms@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz#333b70e7758b802f3dd0ddfe98bb1ccfef96b6e9" + resolved "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz" integrity sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ== dependencies: postcss-value-parser "^4.2.0" postcss-replace-overflow-wrap@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz#d2df6bed10b477bf9c52fab28c568b4b29ca4319" + resolved "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz" integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== postcss-selector-not@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-5.0.0.tgz#ac5fc506f7565dd872f82f5314c0f81a05630dc7" + resolved "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-5.0.0.tgz" integrity sha512-/2K3A4TCP9orP4TNS7u3tGdRFVKqz/E6pX3aGnriPG0jU78of8wsUcqE4QAhWEU0d+WnMSF93Ah3F//vUtK+iQ== dependencies: balanced-match "^1.0.0" postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9: version "6.0.9" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz#ee71c3b9ff63d9cd130838876c13a2ec1a992b2f" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz" integrity sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ== dependencies: cssesc "^3.0.0" @@ -7415,7 +8331,7 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector postcss-svgo@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d" + resolved "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz" integrity sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA== dependencies: postcss-value-parser "^4.2.0" @@ -7423,19 +8339,19 @@ postcss-svgo@^*: postcss-unique-selectors@^*: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz#a9f273d1eacd09e9aa6088f4b0507b18b1b541b6" + resolved "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz" integrity sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA== dependencies: postcss-selector-parser "^6.0.5" postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== postcss@^7.0.35: version "7.0.39" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" + resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz" integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== dependencies: picocolors "^0.2.1" @@ -7443,7 +8359,7 @@ postcss@^7.0.35: postcss@^8.3.5, postcss@^8.4.4, postcss@^8.4.6, postcss@^8.4.7: version "8.4.12" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.12.tgz#1e7de78733b28970fa4743f7da6f3763648b1905" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.12.tgz" integrity sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg== dependencies: nanoid "^3.3.1" @@ -7452,34 +8368,34 @@ postcss@^8.3.5, postcss@^8.4.4, postcss@^8.4.6, postcss@^8.4.7: prelude-ls@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== prelude-ls@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= prettier-linter-helpers@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + resolved "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz" integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== dependencies: fast-diff "^1.1.2" -prettier@^2.5.1: - version "2.6.0" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.0.tgz#12f8f504c4d8ddb76475f441337542fa799207d4" - integrity sha512-m2FgJibYrBGGgQXNzfd0PuDGShJgRavjUoRCw1mZERIWVSXF0iLzLm+aOqTAbLnC3n6JzUhAA8uZnFVghHJ86A== +prettier@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032" + integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew== pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: version "5.6.0" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== pretty-error@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" + resolved "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz" integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw== dependencies: lodash "^4.17.20" @@ -7487,28 +8403,38 @@ pretty-error@^4.0.0: pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz" integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== dependencies: ansi-regex "^5.0.1" ansi-styles "^5.0.0" react-is "^17.0.1" +pretty-format@^28.1.0: + version "28.1.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-28.1.0.tgz#8f5836c6a0dfdb834730577ec18029052191af55" + integrity sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q== + dependencies: + "@jest/schemas" "^28.0.2" + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^18.0.0" + process-nextick-args@~2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== promise@^8.1.0: version "8.1.0" - resolved "https://registry.yarnpkg.com/promise/-/promise-8.1.0.tgz#697c25c3dfe7435dd79fcd58c38a135888eaf05e" + resolved "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz" integrity sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q== dependencies: asap "~2.0.6" prompts@^2.0.1, prompts@^2.4.2: version "2.4.2" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== dependencies: kleur "^3.0.3" @@ -7516,7 +8442,7 @@ prompts@^2.0.1, prompts@^2.4.2: prop-types-exact@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869" + resolved "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz" integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA== dependencies: has "^1.0.3" @@ -7525,15 +8451,15 @@ prop-types-exact@^1.2.0: prop-types-extra@^1.1.0: version "1.1.1" - resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.1.1.tgz#58c3b74cbfbb95d304625975aa2f0848329a010b" + resolved "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz" integrity sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew== dependencies: react-is "^16.3.2" warning "^4.0.0" -prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== dependencies: loose-envify "^1.4.0" @@ -7542,7 +8468,7 @@ prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: proxy-addr@~2.0.7: version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== dependencies: forwarded "0.2.0" @@ -7550,49 +8476,54 @@ proxy-addr@~2.0.7: psl@^1.1.33: version "1.8.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== q@^1.1.2: version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= qs@6.9.7: version "6.9.7" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" + resolved "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz" integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== queue-microtask@^1.2.2: version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== quick-lru@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== +raf-schd@^4.0.2: + version "4.0.3" + resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== + raf@^3.4.1: version "3.4.1" - resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== dependencies: performance-now "^2.1.0" railroad-diagrams@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" + resolved "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz" integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234= randexp@0.4.6: version "0.4.6" - resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" + resolved "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz" integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== dependencies: discontinuous-range "1.0.0" @@ -7600,19 +8531,19 @@ randexp@0.4.6: randombytes@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== raw-body@2.4.3: version "2.4.3" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.3.tgz#8f80305d11c2a0a545c2d9d89d7a0286fcead43c" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz" integrity sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g== dependencies: bytes "3.1.2" @@ -7622,7 +8553,7 @@ raw-body@2.4.3: react-app-polyfill@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz#95221e0a9bd259e5ca6b177c7bb1cb6768f68fd7" + resolved "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz" integrity sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w== dependencies: core-js "^3.19.2" @@ -7632,9 +8563,22 @@ react-app-polyfill@^3.0.0: regenerator-runtime "^0.13.9" whatwg-fetch "^3.6.2" +react-beautiful-dnd@^13.1.0: + version "13.1.0" + resolved "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz" + integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA== + dependencies: + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-bootstrap-typeahead@^6.0.0-alpha.11: version "6.0.0-alpha.11" - resolved "https://registry.yarnpkg.com/react-bootstrap-typeahead/-/react-bootstrap-typeahead-6.0.0-alpha.11.tgz#6476df85256ad6dfe612913db753b52f3c70fef7" + resolved "https://registry.npmjs.org/react-bootstrap-typeahead/-/react-bootstrap-typeahead-6.0.0-alpha.11.tgz" integrity sha512-yHBPsdkAdvvLpkq6wWei55qt4REdbRnC+1wVxkBSBeTG4Z6lkKKSGx6w4kY9YmkyyVcbmmfwUSXhCWY/M+TzCg== dependencies: "@babel/runtime" "^7.14.6" @@ -7650,19 +8594,15 @@ react-bootstrap-typeahead@^6.0.0-alpha.11: scroll-into-view-if-needed "^2.2.20" warning "^4.0.1" -react-bootstrap@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-2.2.1.tgz#2a6ad0931e9367882ec3fc88a70ed0b8ace90b26" - integrity sha512-x8lpVQflsbevphuWbTnTNCatcbKyPJNrP2WyQ1MJYmFEcVjbTbai1yZhdlXr0QUxLQLxA8g5hQWb5TwJtaZoCA== +react-bootstrap@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-2.4.0.tgz#99bf9656e2e7a23ae1ae135d18fd5ad7c344b416" + integrity sha512-dn599jNK1Fg5GGjJH+lQQDwELVzigh/MdusKpB/0el+sCjsO5MZDH5gRMmBjRhC+vb7VlCDr6OXffPIDSkNMLw== dependencies: "@babel/runtime" "^7.17.2" - "@restart/hooks" "^0.4.5" - "@restart/ui" "^1.0.2" - "@types/invariant" "^2.2.35" - "@types/prop-types" "^15.7.4" - "@types/react" ">=16.14.8" + "@restart/hooks" "^0.4.6" + "@restart/ui" "^1.2.0" "@types/react-transition-group" "^4.4.4" - "@types/warning" "^3.0.0" classnames "^2.3.1" dom-helpers "^5.2.1" invariant "^2.2.4" @@ -7674,13 +8614,13 @@ react-bootstrap@^2.2.1: react-collapsible@^2.8.4: version "2.8.4" - resolved "https://registry.yarnpkg.com/react-collapsible/-/react-collapsible-2.8.4.tgz#319ff7471138c4381ce0afa3ac308ccde7f4e09f" + resolved "https://registry.npmjs.org/react-collapsible/-/react-collapsible-2.8.4.tgz" integrity sha512-oG4yOk6AGKswe0OD/8t3/nf4Rgj4UhlZUUvqL5jop0/ez02B3dBDmNvs3sQz0PcTpJvt0ai8zF7Atd1SzN/UNw== -react-dev-utils@^12.0.0: - version "12.0.0" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.0.tgz#4eab12cdb95692a077616770b5988f0adf806526" - integrity sha512-xBQkitdxozPxt1YZ9O1097EJiVpwHr9FoAuEVURCKV0Av8NBERovJauzP7bo1ThvuhZ4shsQ1AJiu4vQpoT1AQ== +react-dev-utils@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" + integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ== dependencies: "@babel/code-frame" "^7.16.0" address "^1.1.2" @@ -7701,7 +8641,7 @@ react-dev-utils@^12.0.0: open "^8.4.0" pkg-up "^3.1.0" prompts "^2.4.2" - react-error-overlay "^6.0.10" + react-error-overlay "^6.0.11" recursive-readdir "^2.2.2" shell-quote "^1.7.3" strip-ansi "^6.0.1" @@ -7709,53 +8649,58 @@ react-dev-utils@^12.0.0: react-dom@^17.0.2: version "17.0.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" scheduler "^0.20.2" -react-error-overlay@^6.0.10: - version "6.0.10" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6" - integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA== +react-error-overlay@^6.0.11: + version "6.0.11" + resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" + integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== react-fast-compare@^3.0.1: version "3.2.0" - resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" + resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz" integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== react-icons@^4.3.1: version "4.3.1" - resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.3.1.tgz#2fa92aebbbc71f43d2db2ed1aed07361124e91ca" + resolved "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz" integrity sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ== react-infinite-scroller@^1.2.6: version "1.2.6" - resolved "https://registry.yarnpkg.com/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz#8b80233226dc753a597a0eb52621247f49b15f18" + resolved "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz" integrity sha512-mGdMyOD00YArJ1S1F3TVU9y4fGSfVVl6p5gh/Vt4u99CJOptfVu/q5V/Wlle72TMgYlBwIhbxK5wF0C/R33PXQ== dependencies: prop-types "^15.5.8" react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.6: version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.1: +react-is@^17.0.1, react-is@^17.0.2: version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-is@^18.0.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" + integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== + react-lifecycles-compat@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== react-overlays@^5.1.0: version "5.1.1" - resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-5.1.1.tgz#2e7cf49744b56537c7828ccb94cfc63dd778ae4f" + resolved "https://registry.npmjs.org/react-overlays/-/react-overlays-5.1.1.tgz" integrity sha512-eCN2s2/+GVZzpnId4XVWtvDPYYBD2EtOGP74hE+8yDskPzFy9+pV1H3ZZihxuRdEbQzzacySaaDkR7xE0ydl4Q== dependencies: "@babel/runtime" "^7.13.8" @@ -7769,43 +8714,55 @@ react-overlays@^5.1.0: react-popper@^2.2.5: version "2.2.5" - resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.5.tgz#1214ef3cec86330a171671a4fbcbeeb65ee58e96" + resolved "https://registry.npmjs.org/react-popper/-/react-popper-2.2.5.tgz" integrity sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw== dependencies: react-fast-compare "^3.0.1" warning "^4.0.2" +react-redux@^7.2.0: + version "7.2.8" + resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.8.tgz" + integrity sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw== + dependencies: + "@babel/runtime" "^7.15.4" + "@types/react-redux" "^7.1.20" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^17.0.2" + react-refresh@^0.11.0: version "0.11.0" - resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" + resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== react-router-bootstrap@^0.26.1: version "0.26.1" - resolved "https://registry.yarnpkg.com/react-router-bootstrap/-/react-router-bootstrap-0.26.1.tgz#395f8d134a58db4e5d4a7ccc37960077414e411f" + resolved "https://registry.npmjs.org/react-router-bootstrap/-/react-router-bootstrap-0.26.1.tgz" integrity sha512-qgFDfnN2U5RBARTf4L/Xrk0xjNid4wRtBQFrzI2Z2Gu74nlfKT5qZreUxDiYvv4sI+NrWcYacRZEqS5yeLYH0A== dependencies: prop-types "^15.7.2" -react-router-dom@^6.2.1: - version "6.2.2" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.2.2.tgz#f1a2c88365593c76b9612ae80154a13fcb72e442" - integrity sha512-AtYEsAST7bDD4dLSQHDnk/qxWLJdad5t1HFa1qJyUrCeGgEuCSw0VB/27ARbF9Fi/W5598ujvJOm3ujUCVzuYQ== +react-router-dom@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d" + integrity sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw== dependencies: history "^5.2.0" - react-router "6.2.2" + react-router "6.3.0" -react-router@6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.2.2.tgz#495e683a0c04461eeb3d705fe445d6cf42f0c249" - integrity sha512-/MbxyLzd7Q7amp4gDOGaYvXwhEojkJD5BtExkuKmj39VEE0m3l/zipf6h2WIB2jyAO0lI6NGETh4RDcktRm4AQ== +react-router@6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" + integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== dependencies: history "^5.2.0" -react-scripts@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.0.tgz#6547a6d7f8b64364ef95273767466cc577cb4b60" - integrity sha512-3i0L2CyIlROz7mxETEdfif6Sfhh9Lfpzi10CtcGs1emDQStmZfWjJbAIMtRD0opVUjQuFWqHZyRZ9PPzKCFxWg== +react-scripts@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003" + integrity sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ== dependencies: "@babel/core" "^7.16.0" "@pmmmwh/react-refresh-webpack-plugin" "^0.5.3" @@ -7823,7 +8780,7 @@ react-scripts@^5.0.0: dotenv "^10.0.0" dotenv-expand "^5.1.0" eslint "^8.3.0" - eslint-config-react-app "^7.0.0" + eslint-config-react-app "^7.0.1" eslint-webpack-plugin "^3.1.1" file-loader "^6.2.0" fs-extra "^10.0.0" @@ -7840,7 +8797,7 @@ react-scripts@^5.0.0: postcss-preset-env "^7.0.1" prompts "^2.4.2" react-app-polyfill "^3.0.0" - react-dev-utils "^12.0.0" + react-dev-utils "^12.0.1" react-refresh "^0.11.0" resolve "^1.20.0" resolve-url-loader "^4.0.0" @@ -7857,14 +8814,27 @@ react-scripts@^5.0.0: optionalDependencies: fsevents "^2.3.2" +react-select@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.3.2.tgz#ecee0d5c59ed4acb7f567f7de3c75a488d93dacb" + integrity sha512-W6Irh7U6Ha7p5uQQ2ZnemoCQ8mcfgOtHfw3wuMzG6FAu0P+CYicgofSLOq97BhjMx8jS+h+wwWdCBeVVZ9VqlQ== + dependencies: + "@babel/runtime" "^7.12.0" + "@emotion/cache" "^11.4.0" + "@emotion/react" "^11.8.1" + "@types/react-transition-group" "^4.4.0" + memoize-one "^5.0.0" + prop-types "^15.6.0" + react-transition-group "^4.3.0" + react-social-login-buttons@^3.6.0: version "3.6.0" - resolved "https://registry.yarnpkg.com/react-social-login-buttons/-/react-social-login-buttons-3.6.0.tgz#2be1cb114d8c0200581ba1c8ec5ea74e89cf7701" + resolved "https://registry.npmjs.org/react-social-login-buttons/-/react-social-login-buttons-3.6.0.tgz" integrity sha512-m5E72jHWgC4VBxRziZYQC5kQIzooGRF+dDE97K5JgSlcDPXkNxCjCzP+Qp9fNhNujG7APvPx2Qhzi1BO2xi17Q== react-test-renderer@^16.0.0-0: version "16.14.0" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae" + resolved "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.14.0.tgz" integrity sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg== dependencies: object-assign "^4.1.1" @@ -7872,9 +8842,16 @@ react-test-renderer@^16.0.0-0: react-is "^16.8.6" scheduler "^0.19.1" -react-transition-group@^4.4.2: +react-toastify@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.0.1.tgz#2f3abd26a75efd55a82cb9c0a897c865218ea420" + integrity sha512-c2zeZHkCX+WXuItS/JRqQ/8CH8Qm/je+M0rt09xe9fnu5YPJigtNOdD8zX4fwLA093V2am3abkGfOowwpkrwOQ== + dependencies: + clsx "^1.1.1" + +react-transition-group@^4.3.0, react-transition-group@^4.4.2: version "4.4.2" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" + resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz" integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg== dependencies: "@babel/runtime" "^7.5.5" @@ -7884,7 +8861,7 @@ react-transition-group@^4.4.2: react@^17.0.2: version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz" integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== dependencies: loose-envify "^1.1.0" @@ -7892,12 +8869,12 @@ react@^17.0.2: reactjs-popup@^2.0.5: version "2.0.5" - resolved "https://registry.yarnpkg.com/reactjs-popup/-/reactjs-popup-2.0.5.tgz#588a74966bb126699429d739948e3448d7771eac" + resolved "https://registry.npmjs.org/reactjs-popup/-/reactjs-popup-2.0.5.tgz" integrity sha512-b5hv9a6aGsHEHXFAgPO5s1Jw1eSkopueyUVxQewGdLgqk2eW0IVXZrPRpHR629YcgIpC2oxtX8OOZ8a7bQJbxA== readable-stream@^2.0.1: version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== dependencies: core-util-is "~1.0.0" @@ -7910,7 +8887,7 @@ readable-stream@^2.0.1: readable-stream@^3.0.6: version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== dependencies: inherits "^2.0.3" @@ -7919,63 +8896,70 @@ readable-stream@^3.0.6: readdirp@~3.6.0: version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: picomatch "^2.2.1" recursive-readdir@^2.2.2: version "2.2.2" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" + resolved "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz" integrity sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg== dependencies: minimatch "3.0.4" redent@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz" integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== dependencies: indent-string "^4.0.0" strip-indent "^3.0.0" +redux@^4.0.0, redux@^4.0.4: + version "4.2.0" + resolved "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz" + integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== + dependencies: + "@babel/runtime" "^7.9.2" + reflect.ownkeys@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" + resolved "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz" integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA= regenerate-unicode-properties@^10.0.1: version "10.0.1" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56" + resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz" integrity sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw== dependencies: regenerate "^1.4.2" regenerate@^1.4.2: version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9: version "0.13.9" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== regenerator-transform@^0.14.2: version "0.14.5" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" + resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz" integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== dependencies: "@babel/runtime" "^7.8.4" regex-parser@^2.2.11: version "2.2.11" - resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58" + resolved "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz" integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.1: version "1.4.1" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz#b3f4c0059af9e47eca9f3f660e51d81307e72307" + resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz" integrity sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ== dependencies: call-bind "^1.0.2" @@ -7983,12 +8967,12 @@ regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.1: regexpp@^3.0.0, regexpp@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== regexpu-core@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.0.1.tgz#c531122a7840de743dcf9c83e923b5560323ced3" + resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.0.1.tgz" integrity sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw== dependencies: regenerate "^1.4.2" @@ -8000,24 +8984,24 @@ regexpu-core@^5.0.1: regjsgen@^0.6.0: version "0.6.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.6.0.tgz#83414c5354afd7d6627b16af5f10f41c4e71808d" + resolved "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz" integrity sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA== regjsparser@^0.8.2: version "0.8.4" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.8.4.tgz#8a14285ffcc5de78c5b95d62bbf413b6bc132d5f" + resolved "https://registry.npmjs.org/regjsparser/-/regjsparser-0.8.4.tgz" integrity sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA== dependencies: jsesc "~0.5.0" relateurl@^0.2.7: version "0.2.7" - resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + resolved "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz" integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= renderkid@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" + resolved "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz" integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg== dependencies: css-select "^4.1.3" @@ -8028,39 +9012,39 @@ renderkid@^3.0.0: require-directory@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= require-from-string@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== requires-port@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= resolve-cwd@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== dependencies: resolve-from "^5.0.0" resolve-from@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== resolve-from@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve-url-loader@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz#d50d4ddc746bb10468443167acf800dcd6c3ad57" + resolved "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz" integrity sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA== dependencies: adjust-sourcemap-loader "^4.0.0" @@ -8071,12 +9055,12 @@ resolve-url-loader@^4.0.0: resolve.exports@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" + resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz" integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== -resolve@^1.10.1, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0: +resolve@^1.10.1, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0: version "1.22.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz" integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== dependencies: is-core-module "^2.8.1" @@ -8085,7 +9069,7 @@ resolve@^1.10.1, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.2 resolve@^2.0.0-next.3: version "2.0.0-next.3" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46" + resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz" integrity sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q== dependencies: is-core-module "^2.2.0" @@ -8093,29 +9077,29 @@ resolve@^2.0.0-next.3: ret@~0.1.10: version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + resolved "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== retry@^0.13.1: version "0.13.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== reusify@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" rollup-plugin-terser@^7.0.0: version "7.0.2" - resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" + resolved "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz" integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== dependencies: "@babel/code-frame" "^7.10.4" @@ -8125,14 +9109,14 @@ rollup-plugin-terser@^7.0.0: rollup@^2.43.1: version "2.70.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.70.1.tgz#824b1f1f879ea396db30b0fc3ae8d2fead93523e" + resolved "https://registry.npmjs.org/rollup/-/rollup-2.70.1.tgz" integrity sha512-CRYsI5EuzLbXdxC6RnYhOuRdtz4bhejPMSWjsFLfVM/7w/85n2szZv6yExqUXsBdz5KT8eoubeyDUDjhLHEslA== optionalDependencies: fsevents "~2.3.2" rst-selector-parser@^2.2.3: version "2.2.3" - resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" + resolved "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz" integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE= dependencies: lodash.flattendeep "^4.4.0" @@ -8140,34 +9124,34 @@ rst-selector-parser@^2.2.3: run-parallel@^1.1.9: version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== dependencies: queue-microtask "^1.2.2" safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sanitize.css@*: version "13.0.0" - resolved "https://registry.yarnpkg.com/sanitize.css/-/sanitize.css-13.0.0.tgz#2675553974b27964c75562ade3bd85d79879f173" + resolved "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz" integrity sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA== sass-loader@^12.3.0: version "12.6.0" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.6.0.tgz#5148362c8e2cdd4b950f3c63ac5d16dbfed37bcb" + resolved "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz" integrity sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA== dependencies: klona "^2.0.4" @@ -8175,19 +9159,19 @@ sass-loader@^12.3.0: sax@~1.2.4: version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== saxes@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" + resolved "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz" integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== dependencies: xmlchars "^2.2.0" scheduler@^0.19.1: version "0.19.1" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz" integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== dependencies: loose-envify "^1.1.0" @@ -8195,7 +9179,7 @@ scheduler@^0.19.1: scheduler@^0.20.2: version "0.20.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz" integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== dependencies: loose-envify "^1.1.0" @@ -8203,7 +9187,7 @@ scheduler@^0.20.2: schema-utils@2.7.0: version "2.7.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz" integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== dependencies: "@types/json-schema" "^7.0.4" @@ -8212,7 +9196,7 @@ schema-utils@2.7.0: schema-utils@^2.6.5: version "2.7.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz" integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== dependencies: "@types/json-schema" "^7.0.5" @@ -8221,7 +9205,7 @@ schema-utils@^2.6.5: schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz" integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== dependencies: "@types/json-schema" "^7.0.8" @@ -8230,7 +9214,7 @@ schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: schema-utils@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz" integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg== dependencies: "@types/json-schema" "^7.0.9" @@ -8240,48 +9224,48 @@ schema-utils@^4.0.0: scroll-into-view-if-needed@^2.2.20: version "2.2.29" - resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz#551791a84b7e2287706511f8c68161e4990ab885" + resolved "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz" integrity sha512-hxpAR6AN+Gh53AdAimHM6C8oTN1ppwVZITihix+WqalywBeFcQ6LdQP5ABNl26nX8GTEL7VT+b8lKpdqq65wXg== dependencies: compute-scroll-into-view "^1.0.17" select-hose@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= selfsigned@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.0.0.tgz#e927cd5377cbb0a1075302cff8df1042cc2bce5b" + resolved "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.0.tgz" integrity sha512-cUdFiCbKoa1mZ6osuJs2uDHrs0k0oprsKveFiiaBKCNq3SYyb5gs2HxhQyDNLCmL51ZZThqi4YNDpCK6GOP1iQ== dependencies: node-forge "^1.2.0" semver@7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" + resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== semver@^5.7.0, semver@^5.7.1: version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.2, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== +semver@^7.3.2, semver@^7.3.5, semver@^7.3.7: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: lru-cache "^6.0.0" send@0.17.2: version "0.17.2" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820" + resolved "https://registry.npmjs.org/send/-/send-0.17.2.tgz" integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww== dependencies: debug "2.6.9" @@ -8300,21 +9284,21 @@ send@0.17.2: serialize-javascript@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz" integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== dependencies: randombytes "^2.1.0" serialize-javascript@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz" integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== dependencies: randombytes "^2.1.0" serve-index@^1.9.1: version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + resolved "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz" integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= dependencies: accepts "~1.3.4" @@ -8327,7 +9311,7 @@ serve-index@^1.9.1: serve-static@1.14.2: version "1.14.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa" + resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz" integrity sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ== dependencies: encodeurl "~1.0.2" @@ -8337,39 +9321,39 @@ serve-static@1.14.2: setprototypeof@1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== setprototypeof@1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== shallowequal@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: shebang-regex "^3.0.0" shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shell-quote@^1.7.3: version "1.7.3" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" + resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz" integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== shiki@^0.10.1: version "0.10.1" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.10.1.tgz#6f9a16205a823b56c072d0f1a0bcd0f2646bef14" + resolved "https://registry.npmjs.org/shiki/-/shiki-0.10.1.tgz" integrity sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng== dependencies: jsonc-parser "^3.0.0" @@ -8378,36 +9362,36 @@ shiki@^0.10.1: side-channel@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== dependencies: call-bind "^1.0.0" get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.2, signal-exit@^3.0.3: +signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== sisteransi@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== slash@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== slash@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + resolved "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz" integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== sockjs@^0.3.21: version "0.3.24" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + resolved "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz" integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== dependencies: faye-websocket "^0.11.3" @@ -8416,17 +9400,17 @@ sockjs@^0.3.21: source-list-map@^2.0.0, source-list-map@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== source-map-js@^1.0.1, source-map-js@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== source-map-loader@^3.0.0: version "3.0.1" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.1.tgz#9ae5edc7c2d42570934be4c95d1ccc6352eba52d" + resolved "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.1.tgz" integrity sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA== dependencies: abab "^2.0.5" @@ -8435,15 +9419,23 @@ source-map-loader@^3.0.0: source-map-resolve@^0.6.0: version "0.6.0" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" + resolved "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz" integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== dependencies: atob "^2.1.2" decode-uri-component "^0.2.0" +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-support@^0.5.6, source-map-support@~0.5.20: version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" @@ -8451,34 +9443,34 @@ source-map-support@^0.5.6, source-map-support@~0.5.20: source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.5.0: +source-map@^0.5.0, source-map@^0.5.7: version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= source-map@^0.7.3, source-map@~0.7.2: version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== source-map@^0.8.0-beta.0: version "0.8.0-beta.0" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz" integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== dependencies: whatwg-url "^7.0.0" sourcemap-codec@^1.4.8: version "1.4.8" - resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== spdy-transport@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + resolved "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz" integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== dependencies: debug "^4.1.0" @@ -8490,7 +9482,7 @@ spdy-transport@^3.0.0: spdy@^4.0.2: version "4.0.2" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + resolved "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz" integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== dependencies: debug "^4.1.0" @@ -8501,34 +9493,34 @@ spdy@^4.0.2: sprintf-js@~1.0.2: version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= stable@^0.1.8: version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + resolved "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== stack-utils@^2.0.3: version "2.0.5" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" + resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz" integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== dependencies: escape-string-regexp "^2.0.0" stackframe@^1.1.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.1.tgz#1033a3473ee67f08e2f2fc8eba6aef4f845124e1" + resolved "https://registry.npmjs.org/stackframe/-/stackframe-1.2.1.tgz" integrity sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg== "statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= string-length@^4.0.1: version "4.0.2" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== dependencies: char-regex "^1.0.2" @@ -8536,7 +9528,7 @@ string-length@^4.0.1: string-length@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-5.0.1.tgz#3d647f497b6e8e8d41e422f7e0b23bc536c8381e" + resolved "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz" integrity sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow== dependencies: char-regex "^2.0.0" @@ -8544,12 +9536,12 @@ string-length@^5.0.1: string-natural-compare@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" + resolved "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -string-width@^4.1.0, string-width@^4.2.0: +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" @@ -8558,7 +9550,7 @@ string-width@^4.1.0, string-width@^4.2.0: string.prototype.matchall@^4.0.6: version "4.0.7" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz#8e6ecb0d8a1fb1fda470d81acecb2dba057a481d" + resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz" integrity sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg== dependencies: call-bind "^1.0.2" @@ -8572,7 +9564,7 @@ string.prototype.matchall@^4.0.6: string.prototype.trim@^1.2.1: version "1.2.5" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.5.tgz#a587bcc8bfad8cb9829a577f5de30dd170c1682c" + resolved "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.5.tgz" integrity sha512-Lnh17webJVsD6ECeovpVN17RlAKjmz4rF9S+8Y45CkMc/ufVpTkU3vZIyIC7sllQ1FCvObZnnCdNs/HXTUOTlg== dependencies: call-bind "^1.0.2" @@ -8581,7 +9573,7 @@ string.prototype.trim@^1.2.1: string.prototype.trimend@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz" integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== dependencies: call-bind "^1.0.2" @@ -8589,7 +9581,7 @@ string.prototype.trimend@^1.0.4: string.prototype.trimstart@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz" integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== dependencies: call-bind "^1.0.2" @@ -8597,21 +9589,21 @@ string.prototype.trimstart@^1.0.4: string_decoder@^1.1.1: version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: safe-buffer "~5.2.0" string_decoder@~1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== dependencies: safe-buffer "~5.1.0" stringify-object@^3.3.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + resolved "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz" integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== dependencies: get-own-enumerable-property-symbols "^3.0.0" @@ -8620,63 +9612,63 @@ stringify-object@^3.3.0: strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" strip-ansi@^7.0.0, strip-ansi@^7.0.1: version "7.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz" integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== dependencies: ansi-regex "^6.0.1" strip-bom@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= strip-bom@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== strip-comments@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b" + resolved "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz" integrity sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw== strip-final-newline@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== strip-indent@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz" integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== dependencies: min-indent "^1.0.0" strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== style-loader@^3.3.1: version "3.3.1" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" + resolved "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz" integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== -styled-components@^5.3.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.3.tgz#312a3d9a549f4708f0fb0edc829eb34bde032743" - integrity sha512-++4iHwBM7ZN+x6DtPPWkCI4vdtwumQ+inA/DdAsqYd4SVgUKJie5vXyzotA00ttcFdQkCng7zc6grwlfIfw+lw== +styled-components@^5.3.5: + version "5.3.5" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.5.tgz#a750a398d01f1ca73af16a241dec3da6deae5ec4" + integrity sha512-ndETJ9RKaaL6q41B69WudeqLzOpY1A/ET/glXkNZ2T7dPjPqpPCXXQjDFYZWwNnE5co0wX+gTCqx9mfxTmSIPg== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/traverse" "^7.4.5" - "@emotion/is-prop-valid" "^0.8.8" + "@emotion/is-prop-valid" "^1.1.0" "@emotion/stylis" "^0.8.4" "@emotion/unitless" "^0.7.4" babel-plugin-styled-components ">= 1.12.0" @@ -8687,36 +9679,41 @@ styled-components@^5.3.3: stylehacks@^*: version "5.1.0" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.0.tgz#a40066490ca0caca04e96c6b02153ddc39913520" + resolved "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.0.tgz" integrity sha512-SzLmvHQTrIWfSgljkQCw2++C9+Ne91d/6Sp92I8c5uHTcy/PgeHamwITIbBW9wnFTY/3ZfSXR9HIL6Ikqmcu6Q== dependencies: browserslist "^4.16.6" postcss-selector-parser "^6.0.4" +stylis@4.0.13: + version "4.0.13" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91" + integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag== + supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: has-flag "^3.0.0" supports-color@^7.0.0, supports-color@^7.1.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" supports-color@^8.0.0: version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: has-flag "^4.0.0" supports-hyperlinks@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" + resolved "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz" integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ== dependencies: has-flag "^4.0.0" @@ -8724,17 +9721,17 @@ supports-hyperlinks@^2.0.0: supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== svg-parser@^2.0.2: version "2.0.4" - resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" + resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz" integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== svgo@^1.2.2: version "1.3.2" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" + resolved "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz" integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== dependencies: chalk "^2.4.1" @@ -8753,7 +9750,7 @@ svgo@^1.2.2: svgo@^2.7.0: version "2.8.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + resolved "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz" integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== dependencies: "@trysound/sax" "0.2.0" @@ -8766,12 +9763,12 @@ svgo@^2.7.0: symbol-tree@^3.2.4: version "3.2.4" - resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== tailwindcss@^3.0.2: version "3.0.23" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.0.23.tgz#c620521d53a289650872a66adfcb4129d2200d10" + resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.23.tgz" integrity sha512-+OZOV9ubyQ6oI2BXEhzw4HrqvgcARY38xv3zKcjnWtMIZstEsXdI9xftd1iB7+RbOnj2HOEzkA0OyB5BaSxPQA== dependencies: arg "^5.0.1" @@ -8798,22 +9795,22 @@ tailwindcss@^3.0.2: tapable@^1.0.0: version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + resolved "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: version "2.2.1" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== temp-dir@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" + resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz" integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== tempy@^0.6.0: version "0.6.0" - resolved "https://registry.yarnpkg.com/tempy/-/tempy-0.6.0.tgz#65e2c35abc06f1124a97f387b08303442bde59f3" + resolved "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz" integrity sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw== dependencies: is-stream "^2.0.0" @@ -8823,7 +9820,7 @@ tempy@^0.6.0: terminal-link@^2.0.0: version "2.1.1" - resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" + resolved "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz" integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== dependencies: ansi-escapes "^4.2.1" @@ -8831,7 +9828,7 @@ terminal-link@^2.0.0: terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5: version "5.3.1" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz#0320dcc270ad5372c1e8993fabbd927929773e54" + resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz" integrity sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g== dependencies: jest-worker "^27.4.5" @@ -8842,7 +9839,7 @@ terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5: terser@^5.0.0, terser@^5.10.0, terser@^5.7.2: version "5.12.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.12.1.tgz#4cf2ebed1f5bceef5c83b9f60104ac4a78b49e9c" + resolved "https://registry.npmjs.org/terser/-/terser-5.12.1.tgz" integrity sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ== dependencies: acorn "^8.5.0" @@ -8852,7 +9849,7 @@ terser@^5.0.0, terser@^5.10.0, terser@^5.7.2: test-exclude@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== dependencies: "@istanbuljs/schema" "^0.1.2" @@ -8861,49 +9858,54 @@ test-exclude@^6.0.0: text-table@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= throat@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" + resolved "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz" integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== thunky@^1.0.2: version "1.1.0" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== timsort@^0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" + resolved "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= +tiny-invariant@^1.0.6: + version "1.2.0" + resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz" + integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg== + tmpl@1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== to-fast-properties@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= to-regex-range@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== dependencies: is-number "^7.0.0" toidentifier@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== tough-cookie@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz" integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== dependencies: psl "^1.1.33" @@ -8912,26 +9914,26 @@ tough-cookie@^4.0.0: tr46@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + resolved "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz" integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= dependencies: punycode "^2.1.0" tr46@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" + resolved "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz" integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== dependencies: punycode "^2.1.1" tryer@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" + resolved "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz" integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== tsconfig-paths@^3.12.0: version "3.14.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.0.tgz#4fcc48f9ccea8826c41b9ca093479de7f5018976" + resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.0.tgz" integrity sha512-cg/1jAZoL57R39+wiw4u/SCC6Ic9Q5NqjBOb+9xISedOYurfog9ZNmKJSxAnb2m/5Bq4lE9lhUcau33Ml8DM0g== dependencies: "@types/json5" "^0.0.29" @@ -8941,58 +9943,58 @@ tsconfig-paths@^3.12.0: tslib@^1.8.1: version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tslib@^2.0.3, tslib@^2.2.0: version "2.3.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== tsutils@^3.21.0: version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== dependencies: tslib "^1.8.1" type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== dependencies: prelude-ls "^1.2.1" type-check@~0.3.2: version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz" integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= dependencies: prelude-ls "~1.1.2" type-detect@4.0.8: version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== type-fest@^0.16.0: version "0.16.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz" integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== type-fest@^0.20.2: version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== type-fest@^0.21.3: version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== type-is@~1.6.18: version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== dependencies: media-typer "0.3.0" @@ -9000,15 +10002,15 @@ type-is@~1.6.18: typedarray-to-buffer@^3.1.5: version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz" integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== dependencies: is-typedarray "^1.0.0" -typedoc@^0.22.13: - version "0.22.13" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.22.13.tgz#d061f8f0fb7c9d686e48814f245bddeea4564e66" - integrity sha512-NHNI7Dr6JHa/I3+c62gdRNXBIyX7P33O9TafGLd07ur3MqzcKgwTvpg18EtvCLHJyfeSthAtCLpM7WkStUmDuQ== +typedoc@^0.22.15: + version "0.22.15" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.22.15.tgz#c6ad7ed9d017dc2c3a06c9189cb392bd8e2d8c3f" + integrity sha512-CMd1lrqQbFvbx6S9G6fL4HKp3GoIuhujJReWqlIvSb2T26vGai+8Os3Mde7Pn832pXYemd9BMuuYWhFpL5st0Q== dependencies: glob "^7.2.0" lunr "^2.3.9" @@ -9016,14 +10018,14 @@ typedoc@^0.22.13: minimatch "^5.0.1" shiki "^0.10.1" -typescript@^4.4.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4" - integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg== +typescript@^4.6.4: + version "4.6.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" + integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== unbox-primitive@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" + resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz" integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== dependencies: function-bind "^1.1.1" @@ -9033,7 +10035,7 @@ unbox-primitive@^1.0.1: uncontrollable@^7.2.1: version "7.2.1" - resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.2.1.tgz#1fa70ba0c57a14d5f78905d533cf63916dc75738" + resolved "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz" integrity sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ== dependencies: "@babel/runtime" "^7.6.3" @@ -9043,12 +10045,12 @@ uncontrollable@^7.2.1: unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz" integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== unicode-match-property-ecmascript@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + resolved "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz" integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== dependencies: unicode-canonical-property-names-ecmascript "^2.0.0" @@ -9056,61 +10058,66 @@ unicode-match-property-ecmascript@^2.0.0: unicode-match-property-value-ecmascript@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714" + resolved "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz" integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== unicode-property-aliases-ecmascript@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" + resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz" integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== unique-string@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz" integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== dependencies: crypto-random-string "^2.0.0" universalify@^0.1.2: version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== universalify@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= unquote@~1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" + resolved "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz" integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= upath@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + resolved "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== uri-js@^4.2.2: version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" +use-memo-one@^1.1.1: + version "1.1.2" + resolved "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz" + integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= util.promisify@~1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" + resolved "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz" integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== dependencies: define-properties "^1.1.3" @@ -9120,79 +10127,88 @@ util.promisify@~1.0.0: utila@~0.4: version "0.4.0" - resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + resolved "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz" integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= utils-merge@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= uuid@^8.3.2: version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== v8-compile-cache@^2.0.3: version "2.3.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" + resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== v8-to-istanbul@^8.1.0: version "8.1.1" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed" + resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz" integrity sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w== dependencies: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" source-map "^0.7.3" +v8-to-istanbul@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.0.tgz#be0dae58719fc53cb97e5c7ac1d7e6d4f5b19511" + integrity sha512-HcvgY/xaRm7isYmyx+lFKA4uQmfUbN0J4M0nNItvzTvH/iQ9kW5j/t4YSR+Ge323/lrgDAWJoF46tzGQHwBHFw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.7" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + vary@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= vscode-oniguruma@^1.6.1: version "1.6.2" - resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz#aeb9771a2f1dbfc9083c8a7fdd9cccaa3f386607" + resolved "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz" integrity sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA== vscode-textmate@5.2.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" + resolved "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz" integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== w3c-hr-time@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" + resolved "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz" integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== dependencies: browser-process-hrtime "^1.0.0" w3c-xmlserializer@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" + resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz" integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA== dependencies: xml-name-validator "^3.0.0" walker@^1.0.7: version "1.0.8" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + resolved "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz" integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== dependencies: makeerror "1.0.12" warning@^4.0.0, warning@^4.0.1, warning@^4.0.2, warning@^4.0.3: version "4.0.3" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + resolved "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz" integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== dependencies: loose-envify "^1.0.0" watchpack@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25" + resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz" integrity sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA== dependencies: glob-to-regexp "^0.4.1" @@ -9200,34 +10216,34 @@ watchpack@^2.3.1: wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + resolved "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz" integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== dependencies: minimalistic-assert "^1.0.0" web-vitals@^2.1.0: version "2.1.4" - resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.4.tgz#76563175a475a5e835264d373704f9dde718290c" + resolved "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz" integrity sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg== webidl-conversions@^4.0.2: version "4.0.2" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== webidl-conversions@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz" integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== webidl-conversions@^6.1.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== webpack-dev-middleware@^5.3.1: version "5.3.1" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz#aa079a8dedd7e58bfeab358a9af7dab304cee57f" + resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz" integrity sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg== dependencies: colorette "^2.0.10" @@ -9238,7 +10254,7 @@ webpack-dev-middleware@^5.3.1: webpack-dev-server@^4.6.0: version "4.7.4" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.7.4.tgz#d0ef7da78224578384e795ac228d8efb63d5f945" + resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.4.tgz" integrity sha512-nfdsb02Zi2qzkNmgtZjkrMOcXnYZ6FLKcQwpxT7MvmHKc+oTtDsBju8j+NMyAygZ9GW1jMEUpy3itHtqgEhe1A== dependencies: "@types/bonjour" "^3.5.9" @@ -9274,7 +10290,7 @@ webpack-dev-server@^4.6.0: webpack-manifest-plugin@^4.0.2: version "4.1.1" - resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz#10f8dbf4714ff93a215d5a45bcc416d80506f94f" + resolved "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz" integrity sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow== dependencies: tapable "^2.0.0" @@ -9282,7 +10298,7 @@ webpack-manifest-plugin@^4.0.2: webpack-sources@^1.4.3: version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz" integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== dependencies: source-list-map "^2.0.0" @@ -9290,7 +10306,7 @@ webpack-sources@^1.4.3: webpack-sources@^2.2.0: version "2.3.1" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.3.1.tgz#570de0af163949fe272233c2cefe1b56f74511fd" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz" integrity sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA== dependencies: source-list-map "^2.0.1" @@ -9298,12 +10314,12 @@ webpack-sources@^2.2.0: webpack-sources@^3.2.3: version "3.2.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack@^5.64.4: version "5.70.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.70.0.tgz#3461e6287a72b5e6e2f4872700bc8de0d7500e6d" + resolved "https://registry.npmjs.org/webpack/-/webpack-5.70.0.tgz" integrity sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw== dependencies: "@types/eslint-scope" "^3.7.3" @@ -9333,7 +10349,7 @@ webpack@^5.64.4: websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== dependencies: http-parser-js ">=0.5.1" @@ -9342,29 +10358,29 @@ websocket-driver@>=0.5.1, websocket-driver@^0.7.4: websocket-extensions@>=0.1.1: version "0.1.4" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== whatwg-encoding@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" + resolved "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz" integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== dependencies: iconv-lite "0.4.24" whatwg-fetch@^3.6.2: version "3.6.2" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" + resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz" integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== whatwg-mimetype@^2.3.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" + resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== whatwg-url@^7.0.0: version "7.1.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz" integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== dependencies: lodash.sortby "^4.7.0" @@ -9373,7 +10389,7 @@ whatwg-url@^7.0.0: whatwg-url@^8.0.0, whatwg-url@^8.5.0: version "8.7.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz" integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== dependencies: lodash "^4.7.0" @@ -9382,7 +10398,7 @@ whatwg-url@^8.0.0, whatwg-url@^8.5.0: which-boxed-primitive@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== dependencies: is-bigint "^1.0.1" @@ -9393,26 +10409,26 @@ which-boxed-primitive@^1.0.2: which@^1.3.1: version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: isexe "^2.0.0" which@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== workbox-background-sync@6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.5.1.tgz#df79c6a4a22945d8a44493a4947a6ed0f720ef86" + resolved "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.5.1.tgz" integrity sha512-T5a35fagLXQvV8Dr4+bDU+XYsP90jJ3eBLjZMKuCNELMQZNj+VekCODz1QK44jgoBeQk+vp94pkZV6G+e41pgg== dependencies: idb "^6.1.4" @@ -9420,14 +10436,14 @@ workbox-background-sync@6.5.1: workbox-broadcast-update@6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.5.1.tgz#9aecb116979b0709480b84cfd1beca7a901d01d4" + resolved "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.5.1.tgz" integrity sha512-mb/oyblyEpDbw167cCTyHnC3RqCnCQHtFYuYZd+QTpuExxM60qZuBH1AuQCgvLtDcztBKdEYK2VFD9SZYgRbaQ== dependencies: workbox-core "6.5.1" workbox-build@6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-6.5.1.tgz#6b5e8f090bb608267868540d3072b44b8531b3bc" + resolved "https://registry.npmjs.org/workbox-build/-/workbox-build-6.5.1.tgz" integrity sha512-coDUDzHvFZ1ADOl3wKCsCSyOBvkPKlPgcQDb6LMMShN1zgF31Mev/1HzN3+9T2cjjWAgFwZKkuRyExqc1v21Zw== dependencies: "@apideck/better-ajv-errors" "^0.3.1" @@ -9470,19 +10486,19 @@ workbox-build@6.5.1: workbox-cacheable-response@6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.5.1.tgz#f71d0a75b3d6846e39594955e99ac42fd26f8693" + resolved "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.5.1.tgz" integrity sha512-3TdtH/luDiytmM+Cn72HCBLZXmbeRNJqZx2yaVOfUZhj0IVwZqQXhNarlGE9/k6U5Jelb+TtpH2mLVhnzfiSMg== dependencies: workbox-core "6.5.1" workbox-core@6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.5.1.tgz#0dba3bccf883a46dfa61cc412eaa3cb09bb549e6" + resolved "https://registry.npmjs.org/workbox-core/-/workbox-core-6.5.1.tgz" integrity sha512-qObXZ39aFJ2N8X7IUbGrJHKWguliCuU1jOXM/I4MTT84u9BiKD2rHMkIzgeRP1Ixu9+cXU4/XHJq3Cy0Qqc5hw== workbox-expiration@6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.5.1.tgz#9f105fcf3362852754884ad153888070ce98b692" + resolved "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.5.1.tgz" integrity sha512-iY/cTADAQATMmPkUBRmQdacqq0TJd2wMHimBQz+tRnPGHSMH+/BoLPABPnu7O7rT/g/s59CUYYRGxe3mEgoJCA== dependencies: idb "^6.1.4" @@ -9490,7 +10506,7 @@ workbox-expiration@6.5.1: workbox-google-analytics@6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.5.1.tgz#685224d439c1e7a943f8241d65e2a34ee95a4ba0" + resolved "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.5.1.tgz" integrity sha512-qZU46/h4dbionYT6Yk6iBkUwpiEzAfnO1W7KkI+AMmY7G9/gA03dQQ7rpTw8F4vWrG7ahTUGWDFv6fERtaw1BQ== dependencies: workbox-background-sync "6.5.1" @@ -9500,14 +10516,14 @@ workbox-google-analytics@6.5.1: workbox-navigation-preload@6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.5.1.tgz#a244e3bdf99ce86da7210315ca1ba5aef3710825" + resolved "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.5.1.tgz" integrity sha512-aKrgAbn2IMgzTowTi/ZyKdQUcES2m++9aGtpxqsX7Gn9ovCY8zcssaMEAMMwrIeveij5HiWNBrmj6MWDHi+0rg== dependencies: workbox-core "6.5.1" workbox-precaching@6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.5.1.tgz#177b6424f1e71e601b9c3d6864decad2655f9ff9" + resolved "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.5.1.tgz" integrity sha512-EzlPBxvmjGfE56YZzsT/vpVkpLG1XJhoplgXa5RPyVWLUL1LbwEAxhkrENElSS/R9tgiTw80IFwysidfUqLihg== dependencies: workbox-core "6.5.1" @@ -9516,14 +10532,14 @@ workbox-precaching@6.5.1: workbox-range-requests@6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.5.1.tgz#f40f84aa8765940543eba16131d02f12b38e2fdc" + resolved "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.5.1.tgz" integrity sha512-57Da/qRbd9v33YlHX0rlSUVFmE4THCjKqwkmfhY3tNLnSKN2L5YBS3qhWeDO0IrMNgUj+rGve2moKYXeUqQt4A== dependencies: workbox-core "6.5.1" workbox-recipes@6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-6.5.1.tgz#d2fb21743677cc3ca9e1fc9e3b68f0d1587df205" + resolved "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.5.1.tgz" integrity sha512-DGsyKygHggcGPQpWafC/Nmbm1Ny3sB2vE9r//3UbeidXiQ+pLF14KEG1/0NNGRaY+lfOXOagq6d1H7SC8KA+rA== dependencies: workbox-cacheable-response "6.5.1" @@ -9535,21 +10551,21 @@ workbox-recipes@6.5.1: workbox-routing@6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.5.1.tgz#5488795ae850fe3ae435241143b54ff25ab0db70" + resolved "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.5.1.tgz" integrity sha512-yAAncdTwanvlR8KPjubyvFKeAok8ZcIws6UKxvIAg0I+wsf7UYi93DXNuZr6RBSQrByrN6HkCyjuhmk8P63+PA== dependencies: workbox-core "6.5.1" workbox-strategies@6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.5.1.tgz#51cabbddad5a1956eb9d51cf6ce01ab0a6372756" + resolved "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.5.1.tgz" integrity sha512-JNaTXPy8wXzKkr+6za7/eJX9opoZk7UgY261I2kPxl80XQD8lMjz0vo9EOcBwvD72v3ZhGJbW84ZaDwFEhFvWA== dependencies: workbox-core "6.5.1" workbox-streams@6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.5.1.tgz#12036817385fa4449a86a3ef77fce1cb00ecad9f" + resolved "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.5.1.tgz" integrity sha512-7jaTWm6HRGJ/ewECnhb+UgjTT50R42E0/uNCC4eTKQwnLO/NzNGjoXTdQgFjo4zteR+L/K6AtFAiYKH3ZJbAYw== dependencies: workbox-core "6.5.1" @@ -9557,12 +10573,12 @@ workbox-streams@6.5.1: workbox-sw@6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-6.5.1.tgz#f9256b40f0a7e94656ccd06f127ba19a92cd23c5" + resolved "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.5.1.tgz" integrity sha512-hVrQa19yo9wzN1fQQ/h2JlkzFpkuH2qzYT2/rk7CLaWt6tLnTJVFCNHlGRRPhytZSf++LoIy7zThT714sowT/Q== workbox-webpack-plugin@^6.4.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-6.5.1.tgz#da88b4b6d8eff855958f0e7ebb7aa3eea50a8282" + resolved "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.5.1.tgz" integrity sha512-SHtlQBpKruI16CAYhICDMkgjXE2fH5Yp+D+1UmBfRVhByZYzusVOykvnPm8ObJb9d/tXgn9yoppoxafFS7D4vQ== dependencies: fast-json-stable-stringify "^2.1.0" @@ -9573,7 +10589,7 @@ workbox-webpack-plugin@^6.4.1: workbox-window@6.5.1: version "6.5.1" - resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-6.5.1.tgz#7b5ca29467b1da45dc9e2b5a1b89159d3eb9957a" + resolved "https://registry.npmjs.org/workbox-window/-/workbox-window-6.5.1.tgz" integrity sha512-oRlun9u7b7YEjo2fIDBqJkU2hXtrEljXcOytRhfeQRbqXxjUOpFgXSGRSAkmDx1MlKUNOSbr+zfi8h5n7In3yA== dependencies: "@types/trusted-types" "^2.0.2" @@ -9581,7 +10597,7 @@ workbox-window@6.5.1: wrap-ansi@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" @@ -9590,12 +10606,12 @@ wrap-ansi@^7.0.0: wrappy@1: version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= write-file-atomic@^3.0.0: version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz" integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== dependencies: imurmurhash "^0.1.4" @@ -9603,54 +10619,67 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" +write-file-atomic@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.1.tgz#9faa33a964c1c85ff6f849b80b42a88c2c537c8f" + integrity sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + ws@^7.4.6: version "7.5.7" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" + resolved "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz" integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== ws@^8.4.2: version "8.5.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + resolved "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz" integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== xml-name-validator@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" + resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== xmlchars@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== xtend@^4.0.2: version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== y18n@^5.0.5: version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== yallist@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== yargs-parser@^20.2.2: version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== +yargs-parser@^21.0.0: + version "21.0.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" + integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== + yargs@^16.2.0: version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== dependencies: cliui "^7.0.2" @@ -9661,7 +10690,20 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@^17.3.1: + version "17.5.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e" + integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.0.0" + yocto-queue@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==