From c896ccdca6e9ddf8ae08384684c83f1efab111ff Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Wed, 16 Mar 2022 16:03:47 +0100 Subject: [PATCH 001/536] start suggestions --- backend/src/database/crud/suggestions.py | 5 +++++ backend/tests/test_database/test_crud/test_suggestions.py | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 backend/src/database/crud/suggestions.py create mode 100644 backend/tests/test_database/test_crud/test_suggestions.py diff --git a/backend/src/database/crud/suggestions.py b/backend/src/database/crud/suggestions.py new file mode 100644 index 000000000..acd89f072 --- /dev/null +++ b/backend/src/database/crud/suggestions.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import Session + +from src.database.models import Suggestion, Student, User + +def create_suggestion(database_session: Session, ) \ No newline at end of file diff --git a/backend/tests/test_database/test_crud/test_suggestions.py b/backend/tests/test_database/test_crud/test_suggestions.py new file mode 100644 index 000000000..0a014b36f --- /dev/null +++ b/backend/tests/test_database/test_crud/test_suggestions.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import Session + +from src.database.models import Suggestion, Student, User + +from src.database.crud.suggestions import create_suggestion \ No newline at end of file From 7fccca115a95a334e32f908d4545bb7e602b7c88 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Wed, 16 Mar 2022 16:14:46 +0100 Subject: [PATCH 002/536] start suggestions --- backend/src/database/crud/suggestions.py | 3 +- backend/tests/fill_database.py | 167 ++++++++++++++++++ .../test_crud/test_suggestions.py | 11 +- 3 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 backend/tests/fill_database.py diff --git a/backend/src/database/crud/suggestions.py b/backend/src/database/crud/suggestions.py index acd89f072..599fbc29c 100644 --- a/backend/src/database/crud/suggestions.py +++ b/backend/src/database/crud/suggestions.py @@ -2,4 +2,5 @@ from src.database.models import Suggestion, Student, User -def create_suggestion(database_session: Session, ) \ No newline at end of file +def create_suggestion(database_session: Session): + pass \ No newline at end of file diff --git a/backend/tests/fill_database.py b/backend/tests/fill_database.py new file mode 100644 index 000000000..8e783d653 --- /dev/null +++ b/backend/tests/fill_database.py @@ -0,0 +1,167 @@ +from sqlalchemy.orm import Session + +from src.database.models import * +from src.database.enums import * +from src.app.logic.security import get_password_hash +from datetime import date + +def fill_database(db: Session): + """A function to fill the database with fake data that can easly be used when testing""" + # Editions + edition: Edition = Edition(year = 2022) + db.add(edition) + db.commit() + + # Users + admin: User = User(name="admin", email="admin@ngmail.com", admin=True) + coach1: User = User(name="coach1", email="coach1@noutlook.be") + coach2: User = User(name="coach2", email="coach2@noutlook.be") + request: User = User(name="request", email="request@ngmail.com") + db.add(admin) + db.add(coach1) + db.add(coach2) + db.add(request) + db.commit() + + # AuthEmail + pw_hash = get_password_hash("wachtwoord") + auth_email_admin: AuthEmail = AuthEmail(user=admin, pw_hash=pw_hash) + auth_email_coach1: AuthEmail = AuthEmail(user=coach1, pw_hash=pw_hash) + auth_email_coach2: AuthEmail = AuthEmail(user=coach2, pw_hash=pw_hash) + auth_email_request: AuthEmail = AuthEmail(user=request, pw_hash=pw_hash) + db.add(auth_email_admin) + db.add(auth_email_coach1) + db.add(auth_email_coach2) + db.add(auth_email_request) + 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") + db.add(skill1) + db.add(skill2) + db.add(skill3) + db.add(skill4) + db.add(skill5) + db.add(skill6) + db.commit() + + #Student + 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, skill6]) + 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, skill5]) + 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, skill4, skill5]) + student04: Student = Student(first_name="Délano", last_name="Van Lienden", preferred_name="Délano",email_address="delano.vanlienden@example.com", phone_number="(128)-049-9143", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + student05: Student = Student(first_name="Einar", last_name="Rossebø", preferred_name="Einar",email_address="einar.rossebo@example.com", phone_number="61491822", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[skill2, skill4, skill5]) + student06: Student = Student(first_name="Dave", last_name="Johnston", preferred_name="Dave",email_address="dave.johnston@example.com", phone_number="031-156-2869", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[skill2, skill4, skill5]) + student07: Student = Student(first_name="Fernando", last_name="Stone", preferred_name="Fernando",email_address="fernando.stone@example.com", phone_number="(441)-156-4776", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + student08: Student = Student(first_name="Isabelle", last_name="Singh", preferred_name="Isabelle",email_address="isabelle.singh@example.com", phone_number="(338)-531-9957", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[skill2, skill4, skill5]) + student09: Student = Student(first_name="Blake", last_name="Martin", preferred_name="Blake",email_address="blake.martin@example.com", phone_number="404-060-5843", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + student10: 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=[skill2]) + student11: Student = Student(first_name="Mehmet", last_name="Balcı", preferred_name="Mehmet",email_address="mehmet.balci@example.com", phone_number="(496)-221-8222", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + student12: Student = Student(first_name="Óscar", last_name="das Neves", preferred_name="Óscar",email_address="oscar.dasneves@example.com", phone_number="(47) 6646-0730", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[skill4]) + student13: Student = Student(first_name="Melike", last_name="Süleymanoğlu", preferred_name="Melike",email_address="melike.suleymanoglu@example.com", phone_number="(274)-545-3055", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[skill2, skill4, skill5]) + student14: Student = Student(first_name="Magnus", last_name="Schanke", preferred_name="Magnus",email_address="magnus.schanke@example.com", phone_number="63507430", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[skill2, skill4, skill5]) + student15: Student = Student(first_name="Tara", last_name="Howell", preferred_name="Tara",email_address="tara.howell@example.com", phone_number="07-9111-0958", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + student16: Student = Student(first_name="Hanni", last_name="Ewers", preferred_name="Hanni",email_address="hanni.ewers@example.com", phone_number="0241-5176890", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill1, skill6, skill5]) + student17: Student = Student(first_name="آیناز", last_name="کریمی", preferred_name="آیناز",email_address="aynz.khrymy@example.com", phone_number="009-26345191", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[skill2, skill4, skill5]) + student18: Student = Student(first_name="Vicente", last_name="Garrido", preferred_name="Vicente",email_address="vicente.garrido@example.com", phone_number="987-381-670", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + student19: Student = Student(first_name="Elmer", last_name="Morris", preferred_name="Elmer",email_address="elmer.morris@example.com", phone_number="(611)-832-8108", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + student20: Student = Student(first_name="Alexis", last_name="Roy", preferred_name="Alexis",email_address="alexis.roy@example.com", phone_number="566-546-7642", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + student21: Student = Student(first_name="Lillie", last_name="Kelly", preferred_name="Lillie",email_address="lillie.kelly@example.com", phone_number="(983)-560-1392", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + student22: Student = Student(first_name="Karola", last_name="Andersen", preferred_name="Karola",email_address="karola.andersen@example.com", phone_number="0393-3219328", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + student23: Student = Student(first_name="Elvine", last_name="Andvik", preferred_name="Elvine",email_address="elvine.andvik@example.com", phone_number="30454610", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[skill2, skill4, skill5]) + student24: Student = Student(first_name="Chris", last_name="Kelly", preferred_name="Chris",email_address="chris.kelly@example.com", phone_number="061-399-0053", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill4]) + student25: Student = Student(first_name="Aada", last_name="Pollari", preferred_name="Aada",email_address="aada.pollari@example.com", phone_number="02-908-609", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + student26: Student = Student(first_name="Sofia", last_name="Haataja", preferred_name="Sofia",email_address="sofia.haataja@example.com", phone_number="06-373-889", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + student27: Student = Student(first_name="Charlene", last_name="Gregory", preferred_name="Charlene",email_address="charlene.gregory@example.com", phone_number="(991)-378-7095", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + student28: Student = Student(first_name="Danielle", last_name="Chavez", preferred_name="Danielle",email_address="danielle.chavez@example.com", phone_number="01435 91142", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + student29: Student = Student(first_name="Nikolaj", last_name="Poulsen", preferred_name="Nikolaj",email_address="nikolaj.poulsen@example.com", phone_number="20525141", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + student30: Student = Student(first_name="Marta", last_name="Marquez", preferred_name="Marta",email_address="marta.marquez@example.com", phone_number="967-895-285", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + student31: Student = Student(first_name="Gül", last_name="Barbarosoğlu", preferred_name="Gül",email_address="gul.barbarosoglu@example.com", phone_number="(008)-316-3264", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + + db.add(student01) + db.add(student02) + db.add(student03) + db.add(student04) + db.add(student05) + db.add(student06) + db.add(student07) + db.add(student08) + db.add(student09) + db.add(student10) + db.add(student11) + db.add(student12) + db.add(student13) + db.add(student14) + db.add(student15) + db.add(student16) + db.add(student17) + db.add(student18) + db.add(student19) + db.add(student20) + db.add(student21) + db.add(student22) + db.add(student23) + db.add(student24) + db.add(student25) + db.add(student26) + db.add(student27) + db.add(student28) + db.add(student29) + db.add(student30) + db.add(student31) + db.commit() + + # CoachRequest + coach_request: CoachRequest = CoachRequest(edition=edition, user=request) + db.add(coach_request) + db.commit() + + #DecisionEmail + decision_email1 : DecisionEmail = DecisionEmail(decision=DecisionEnum.NO, student=student29, date=date.today()) + decision_email2 : DecisionEmail = DecisionEmail(decision=DecisionEnum.YES, student=student09, date=date.today()) + decision_email3 : DecisionEmail = DecisionEmail(decision=DecisionEnum.YES, student=student10, date=date.today()) + decision_email4 : DecisionEmail = DecisionEmail(decision=DecisionEnum.YES, student=student11, date=date.today()) + decision_email5 : DecisionEmail = DecisionEmail(decision=DecisionEnum.YES, student=student12, date=date.today()) + decision_email6 : DecisionEmail = DecisionEmail(decision=DecisionEnum.MAYBE, student=student06, date=date.today()) + decision_email7 : DecisionEmail = DecisionEmail(decision=DecisionEnum.MAYBE, student=student26, date=date.today()) + db.add(decision_email1) + db.add(decision_email2) + db.add(decision_email3) + db.add(decision_email4) + db.add(decision_email5) + db.add(decision_email6) + db.add(decision_email7) + db.commit() + + # InviteLink + inviteLink1: InviteLink = InviteLink(target_email="newuser1@email.com", edition=edition) + inviteLink2: InviteLink = InviteLink(target_email="newuser2@email.com", edition=edition) + db.add(inviteLink1) + db.add(inviteLink2) + db.commit() + + # Partner + partner1: Partner = Partner(name="Partner1") + partner2: Partner = Partner(name="Partner2") + partner3: Partner = Partner(name="Partner3") + db.add(partner1) + db.add(partner2) + db.add(partner3) + db.commit() + + #Project + project1: Project = Project(name="project1", number_of_students=3, edition=edition, partners=[partner1]) + project2: Project = Project(name="project2", number_of_students=6, edition=edition, partners=[partner2]) + project3: Project = Project(name="project3", number_of_students=2, edition=edition, partners=[partner3]) + project4: Project = Project(name="project4", number_of_students=9, edition=edition, partners=[partner1, partner3]) + db.add(project1) + db.add(project2) + db.add(project3) + db.add(project4) + db.commit() + diff --git a/backend/tests/test_database/test_crud/test_suggestions.py b/backend/tests/test_database/test_crud/test_suggestions.py index 0a014b36f..4aa88fc30 100644 --- a/backend/tests/test_database/test_crud/test_suggestions.py +++ b/backend/tests/test_database/test_crud/test_suggestions.py @@ -2,4 +2,13 @@ from src.database.models import Suggestion, Student, User -from src.database.crud.suggestions import create_suggestion \ No newline at end of file +from src.database.crud.suggestions import create_suggestion +from tests.fill_database import fill_database + +def test_create_suggestion(database_session: Session): + fill_database(database_session) + + user: User = database_session.query(User).where(User.name == "coach1").first() + print(user) + + assert False \ No newline at end of file From b7e087a6cf0ee0e8da58c67d25d65b60a92eabd6 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Wed, 16 Mar 2022 21:38:54 +0100 Subject: [PATCH 003/536] about #82 and about #87 --- ...ultiple_column_unique_constraint_coach_.py | 32 ++++++++ backend/src/database/crud/suggestions.py | 10 ++- backend/src/database/models.py | 5 +- .../test_crud/test_suggestions.py | 73 ++++++++++++++++++- 4 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 backend/migrations/versions/8c97ecc58e5f_multiple_column_unique_constraint_coach_.py diff --git a/backend/migrations/versions/8c97ecc58e5f_multiple_column_unique_constraint_coach_.py b/backend/migrations/versions/8c97ecc58e5f_multiple_column_unique_constraint_coach_.py new file mode 100644 index 000000000..91354c241 --- /dev/null +++ b/backend/migrations/versions/8c97ecc58e5f_multiple_column_unique_constraint_coach_.py @@ -0,0 +1,32 @@ +"""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/src/database/crud/suggestions.py b/backend/src/database/crud/suggestions.py index 599fbc29c..31704c66b 100644 --- a/backend/src/database/crud/suggestions.py +++ b/backend/src/database/crud/suggestions.py @@ -1,6 +1,12 @@ from sqlalchemy.orm import Session from src.database.models import Suggestion, Student, User +from src.database.enums import DecisionEnum -def create_suggestion(database_session: Session): - pass \ No newline at end of file +def create_suggestion(db: Session, user: User, student: Student, decision: DecisionEnum, argumentation: str) -> None: + suggestion: Suggestion = Suggestion(student=student, coach=user,suggestion=decision,argumentation=argumentation) + db.add(suggestion) + db.commit() + +def get_suggestions_of_student(db: Session, student_id: int) -> list[Suggestion]: + return db.query(Suggestion).where(Suggestion.student_id == student_id).all() \ No newline at end of file diff --git a/backend/src/database/models.py b/backend/src/database/models.py index 0ea5073cf..3fa2fa04f 100644 --- a/backend/src/database/models.py +++ b/backend/src/database/models.py @@ -13,7 +13,7 @@ from uuid import uuid4, UUID -from sqlalchemy import Column, Integer, Enum, ForeignKey, Text, Boolean, DateTime, Table +from sqlalchemy import Column, Integer, Enum, ForeignKey, Text, Boolean, DateTime, Table, UniqueConstraint from sqlalchemy.orm import declarative_base, relationship from sqlalchemy_utils import UUIDType # type: ignore @@ -273,6 +273,9 @@ 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'), + ) suggestion_id = Column(Integer, primary_key=True) student_id = Column(Integer, ForeignKey("students.student_id"), nullable=False) diff --git a/backend/tests/test_database/test_crud/test_suggestions.py b/backend/tests/test_database/test_crud/test_suggestions.py index 4aa88fc30..d6568d916 100644 --- a/backend/tests/test_database/test_crud/test_suggestions.py +++ b/backend/tests/test_database/test_crud/test_suggestions.py @@ -1,14 +1,79 @@ +import pytest from sqlalchemy.orm import Session +from sqlalchemy.exc import IntegrityError from src.database.models import Suggestion, Student, User -from src.database.crud.suggestions import create_suggestion +from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student from tests.fill_database import fill_database +from src.database.enums import DecisionEnum -def test_create_suggestion(database_session: Session): +def test_create_suggestion_yes(database_session: Session): fill_database(database_session) user: User = database_session.query(User).where(User.name == "coach1").first() - print(user) + student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() + + create_suggestion(database_session, user, student, DecisionEnum.YES, "This is a good student") - assert False \ No newline at end of file + suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user and Suggestion.student == student).first() + + assert suggestion.coach == user + assert suggestion.student == student + assert suggestion.suggestion == DecisionEnum.YES + assert suggestion.argumentation == "This is a good student" + +def test_create_suggestion_no(database_session: Session): + fill_database(database_session) + + user: User = database_session.query(User).where(User.name == "coach1").first() + student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() + + create_suggestion(database_session, user, student, DecisionEnum.NO, "This is a not good student") + + suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user and Suggestion.student == student).first() + + assert suggestion.coach == user + assert suggestion.student == student + assert suggestion.suggestion == DecisionEnum.NO + assert suggestion.argumentation == "This is a not good student" + +def test_create_suggestion_maybe(database_session: Session): + fill_database(database_session) + + user: User = database_session.query(User).where(User.name == "coach1").first() + student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() + + create_suggestion(database_session, user, student, DecisionEnum.MAYBE, "Idk if it's good student") + + suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user and Suggestion.student == student).first() + + assert suggestion.coach == user + assert suggestion.student == student + assert suggestion.suggestion == DecisionEnum.MAYBE + assert suggestion.argumentation == "Idk if it's good student" + +def test_multiple_suggestions_about_same_student(database_session: Session): + fill_database(database_session) + + user: User = database_session.query(User).where(User.name == "coach1").first() + student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() + + create_suggestion(database_session, user, student, DecisionEnum.MAYBE, "Idk if it's good student") + with pytest.raises(IntegrityError): + create_suggestion(database_session, user, student, DecisionEnum.YES, "This is a good student") + +def test_get_suggestions_of_student(database_session: Session): + fill_database(database_session) + + user1: User = database_session.query(User).where(User.name == "coach1").first() + user2: User = database_session.query(User).where(User.name == "coach2").first() + student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() + + create_suggestion(database_session, user1, student, DecisionEnum.MAYBE, "Idk if it's good student") + create_suggestion(database_session, user2, student, DecisionEnum.YES, "This is a good student") + suggestions_student = get_suggestions_of_student(database_session, student.student_id) + + assert len(suggestions_student) == 2 + assert suggestions_student[0].student == student + assert suggestions_student[1].student == student \ No newline at end of file From 2ff2a9fa4d6239fcc7d0ba9b1927bdd4a195199c Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 13 Mar 2022 22:55:10 +0100 Subject: [PATCH 004/536] Upgrade mypy version --- backend/requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index 2f7fa8763..363b16676 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -18,7 +18,7 @@ pytest-env==0.6.2 pylint==2.12.2 # Mypy: check type usage in code -mypy==0.931 +mypy==0.940 # Sqlalchemy-stubs: type hints for sqlalchemy sqlalchemy2-stubs==0.0.2a20 From 10a3bbc8b255b6c4c2d6cfb1977ec1e6dfa174d6 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 13 Mar 2022 23:31:58 +0100 Subject: [PATCH 005/536] Create pyproject toml file, move mypy & requirements over --- backend/mypy.ini | 8 --- backend/pyproject.toml | 95 ++++++++++++++++++++++++++++++++++++ backend/requirements-dev.txt | 36 -------------- backend/requirements.txt | 35 ------------- 4 files changed, 95 insertions(+), 79 deletions(-) delete mode 100644 backend/mypy.ini create mode 100644 backend/pyproject.toml diff --git a/backend/mypy.ini b/backend/mypy.ini deleted file mode 100644 index f5cdb3614..000000000 --- a/backend/mypy.ini +++ /dev/null @@ -1,8 +0,0 @@ -[mypy] -plugins = sqlalchemy.ext.mypy.plugin - -[mypy-sqlalchemy_utils] -ignore_errors = True - -[mypy-jose.*] -ignore_missing_imports = True \ No newline at end of file diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 000000000..62651372e --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,95 @@ +[tool.poetry] +name = "backend" +version = "0.1.0" +description = "backend" +authors = ["Team 3"] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.10" + +# Alembic: Database migrations extension for SQLAlchemy +alembic="1.7.6" + +# Environs: simplified environment variable parsing +environs="9.5.0" + +# FastAPI: API framework +fastapi="0.74.1" + +# MariaDB: Python MariaDB connector +mariadb="1.0.10" + +# Hash passwords +passlib = { "version" = "1.7.4", extras = ["bcrypt"]} + +# Generate and verify JWT tokens +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 + +# OAuth2 form data +python-multipart="0.0.5" + +# Requests: HTTP library +requests="2.27.1" + +# SQLAlchemy: ORM and database toolkit +SQLAlchemy="1.4.31" + +# 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"] } + +[tool.poetry.dev-dependencies] +# Coverage: generate code coverage reports +coverage="6.3.1" + +# faker: Generate dummy data +faker = "13.3.1" + +# Mypy: check type usage in code +mypy="0.940" + +# Pylint: Python linter +pylint="2.12.2" + +# 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-cov: coverage plugin for pytest +pytest-cov="3.0.0" + +# Pytest-env: env plugin for pytest +pytest-env="0.6.2" + +# Pytest-mock: mocking for pytest +pytest-mock = "3.7.0" + +# Sqlalchemy-stubs: type hints for sqlalchemy +sqlalchemy2-stubs="0.0.2a20" + +# Types for the passlib library +types-passlib = "1.7.0" + +[tool.mypy] +plugins = ["sqlalchemy.ext.mypy.plugin"] + +[[tool.mypy.overrides]] +module = "sqlalchemy_utils" +ignore_errors = true + +[[tool.mypy.overrides]] +module = "jose.*" +ignore_missing_imports = true + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index 363b16676..e69de29bb 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -1,36 +0,0 @@ -# Coverage: generate code coverage reports -coverage==6.3.1 - -# Pylint: Python linter -pylint==2.12.2 - -# Pytest: Python testing framework -# (more advanced than the built-in unittest module) -pytest==7.0.1 - -# Pytest-cov: coverage plugin for pytest -pytest-cov==3.0.0 - -# Pytest-env: env plugin for pytest -pytest-env==0.6.2 - -# Pylint: check your codestyle -pylint==2.12.2 - -# Mypy: check type usage in code -mypy==0.940 - -# Sqlalchemy-stubs: type hints for sqlalchemy -sqlalchemy2-stubs==0.0.2a20 - -# Pylint-Pytest: A Pylint plugin to suppress pytest-related false positives. -pylint-pytest==1.1.2 - -# faker: Generate dummy data -faker==13.3.1 - -# Types for the passlib library -types-passlib==1.7.0 - -# Pytest-mock: mocking for pytest -pytest-mock==3.7.0 diff --git a/backend/requirements.txt b/backend/requirements.txt index 364343e9e..e69de29bb 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,35 +0,0 @@ -# Alembic: Database migrations extension for SQLAlchemy -alembic==1.7.6 - -# Environs: simplified environment variable parsing -environs==9.5.0 - -# FastAPI: API framework -fastapi==0.74.1 - -# Uvicorn: ASGI web server implementation -uvicorn[standard] >=0.12.0,<0.16.0 - -# MariaDB: Python MariaDB connector -mariadb==1.0.10 - -# Requests: HTTP library -requests==2.27.1 - -# SQLAlchemy: ORM and database toolkit -SQLAlchemy==1.4.31 - -# SQLAlchemy-Utils: Various utility functions and datatypes for SQLAlchemy -sqlalchemy-utils==0.38.2 - -# Humps: Convert strings (and dictionary keys) between snake case, camel case and pascal case in Python -pyhumps==3.5.3 - -# OAuth2 form data -python-multipart==0.0.5 - -# generate and verify JWT tokens -python-jose[cryptography]==3.3.0 - -# hash passwords -passlib[bcrypt]==1.7.4 From 555c56e8f2ed9322d564df1d14d4571a22ff1e13 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 13 Mar 2022 23:43:03 +0100 Subject: [PATCH 006/536] Add pytest and pylint to toml --- backend/.pylintrc | 11 ----------- backend/pyproject.toml | 21 +++++++++++++++++++++ backend/pytest.ini | 6 ------ 3 files changed, 21 insertions(+), 17 deletions(-) delete mode 100644 backend/.pylintrc delete mode 100644 backend/pytest.ini diff --git a/backend/.pylintrc b/backend/.pylintrc deleted file mode 100644 index 5c3034585..000000000 --- a/backend/.pylintrc +++ /dev/null @@ -1,11 +0,0 @@ -[MASTER] -load-plugins=pylint_pytest -argument-rgx=[a-z_][a-z0-9_]{1,31}$ -disable= - import-outside-toplevel, - missing-module-docstring, - too-few-public-methods, -extension-pkg-whitelist=pydantic - -[FORMAT] -max-line-length=120 \ No newline at end of file diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 62651372e..52123eca2 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -90,6 +90,27 @@ ignore_errors = true module = "jose.*" ignore_missing_imports = true +[tool.pylint.master] +load-plugins=["pylint_pytest"] +argument-rgx = "[a-z_][a-z0-9_]{1,31}$" +disable=[ + "import-outside-toplevel", + "missing-module-docstring", + "too-few-public-methods", +] +extension-pkg-whitelist = "pydantic" + +[tool.pylint.format] +max-line-length = 120 + +[tool.pytest.ini_options] +filterwarnings = [ + "ignore:.*The distutils package is deprecated:DeprecationWarning", +] +env = [ + "DB_USE_SQLITE = 1", +] + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/backend/pytest.ini b/backend/pytest.ini deleted file mode 100644 index 5fa05dfe5..000000000 --- a/backend/pytest.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pytest] -filterwarnings= - ignore:.*The distutils package is deprecated:DeprecationWarning - -env= - DB_USE_SQLITE=1 \ No newline at end of file From f196ee203d277b6269943a7de615afea6e9f7fc3 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 14 Mar 2022 00:06:22 +0100 Subject: [PATCH 007/536] Install dependencies --- backend/poetry.lock | 1605 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1605 insertions(+) create mode 100644 backend/poetry.lock diff --git a/backend/poetry.lock b/backend/poetry.lock new file mode 100644 index 000000000..53864c1ef --- /dev/null +++ b/backend/poetry.lock @@ -0,0 +1,1605 @@ +[[package]] +name = "alembic" +version = "1.7.6" +description = "A database migration tool for SQLAlchemy." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" + +[package.extras] +tz = ["python-dateutil"] + +[[package]] +name = "anyio" +version = "3.5.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +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)"] +trio = ["trio (>=0.16)"] + +[[package]] +name = "asgiref" +version = "3.5.0" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] + +[[package]] +name = "astroid" +version = "2.9.3" +description = "An abstract syntax tree for Python with inference support." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +wrapt = ">=1.11,<1.14" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "bcrypt" +version = "3.2.0" +description = "Modern password hashing for your software and your servers" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.1" +six = ">=1.4.1" + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "cffi" +version = "1.15.0" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.0.4" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "6.3.1" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cryptography" +version = "36.0.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +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)"] + +[[package]] +name = "ecdsa" +version = "0.17.0" +description = "ECDSA cryptographic signature library (pure python)" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +gmpy = ["gmpy"] +gmpy2 = ["gmpy2"] + +[[package]] +name = "environs" +version = "9.5.0" +description = "simplified environment variable parsing" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +marshmallow = ">=3.0.0" +python-dotenv = "*" + +[package.extras] +dev = ["pytest", "dj-database-url", "dj-email-url", "django-cache-url", "flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)", "tox"] +django = ["dj-database-url", "dj-email-url", "django-cache-url"] +lint = ["flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)"] +tests = ["pytest", "dj-database-url", "dj-email-url", "django-cache-url"] + +[[package]] +name = "fastapi" +version = "0.74.1" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "main" +optional = false +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" + +[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)"] + +[[package]] +name = "greenlet" +version = "1.1.2" +description = "Lightweight in-process concurrent programming" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" + +[package.extras] +docs = ["sphinx"] + +[[package]] +name = "h11" +version = "0.13.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 = "httptools" +version = "0.2.0" +description = "A collection of framework independent HTTP protocol utils." +category = "main" +optional = false +python-versions = "*" + +[package.extras] +test = ["Cython (==0.29.22)"] + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "lazy-object-proxy" +version = "1.7.1" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mako" +version = "1.2.0" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["babel"] +lingua = ["lingua"] +testing = ["pytest"] + +[[package]] +name = "mariadb" +version = "1.0.10" +description = "Python MariaDB extension" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "markupsafe" +version = "2.1.0" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "marshmallow" +version = "3.15.0" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +packaging = "*" + +[package.extras] +dev = ["pytest", "pytz", "simplejson", "mypy (==0.940)", "flake8 (==4.0.1)", "flake8-bugbear (==22.1.11)", "pre-commit (>=2.4,<3.0)", "tox"] +docs = ["sphinx (==4.4.0)", "sphinx-issues (==3.0.1)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.7)"] +lint = ["mypy (==0.940)", "flake8 (==4.0.1)", "flake8-bugbear (==22.1.11)", "pre-commit (>=2.4,<3.0)"] +tests = ["pytest", "pytz", "simplejson"] + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mypy" +version = "0.940" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +mypy-extensions = ">=0.4.3" +tomli = ">=1.1.0" +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "passlib" +version = "1.7.4" +description = "comprehensive password hashing framework supporting over 30 schemes" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +bcrypt = {version = ">=3.1.0", optional = true, markers = "extra == \"bcrypt\""} + +[package.extras] +argon2 = ["argon2-cffi (>=18.2.0)"] +bcrypt = ["bcrypt (>=3.1.0)"] +build_docs = ["sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)", "cloud-sptheme (>=1.10.1)"] +totp = ["cryptography"] + +[[package]] +name = "platformdirs" +version = "2.5.1" +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)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyasn1" +version = "0.4.8" +description = "ASN.1 types and codecs" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pydantic" +version = "1.9.0" +description = "Data validation and settings management using python 3.6 type hinting" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pylint" +version = "2.12.2" +description = "python code static checker" +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +astroid = ">=2.9.0,<2.10" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.7" +platformdirs = ">=2.2.0" +toml = ">=0.9.2" + +[[package]] +name = "pylint-pytest" +version = "1.1.2" +description = "A Pylint plugin to suppress pytest-related false positives." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pylint = "*" +pytest = ">=4.6" + +[[package]] +name = "pyparsing" +version = "3.0.7" +description = "Python parsing module" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "7.0.1" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-env" +version = "0.6.2" +description = "py.test plugin that allows you to add environment variables." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pytest = ">=2.6.0" + +[[package]] +name = "python-dotenv" +version = "0.19.2" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-jose" +version = "3.3.0" +description = "JOSE implementation in Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryptography\""} +ecdsa = "!=0.15" +pyasn1 = "*" +rsa = "*" + +[package.extras] +cryptography = ["cryptography (>=3.4.0)"] +pycrypto = ["pycrypto (>=2.6.0,<2.7.0)", "pyasn1"] +pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)", "pyasn1"] + +[[package]] +name = "python-multipart" +version = "0.0.5" +description = "A streaming multipart parser for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.4.0" + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "requests" +version = "2.27.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "rsa" +version = "4.8" +description = "Pure-Python RSA implementation" +category = "main" +optional = false +python-versions = ">=3.6,<4" + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "sniffio" +version = "1.2.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "sqlalchemy" +version = "1.4.31" +description = "Database Abstraction Library" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} + +[package.extras] +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)"] +mariadb_connector = ["mariadb (>=1.0.1)"] +mssql = ["pyodbc"] +mssql_pymssql = ["pymssql"] +mssql_pyodbc = ["pyodbc"] +mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"] +mysql_connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"] +postgresql_pg8000 = ["pg8000 (>=1.16.6)"] +postgresql_psycopg2binary = ["psycopg2-binary"] +postgresql_psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql (<1)", "pymysql"] +sqlcipher = ["sqlcipher3-binary"] + +[[package]] +name = "sqlalchemy-utils" +version = "0.38.2" +description = "Various utility functions for SQLAlchemy." +category = "main" +optional = false +python-versions = "~=3.4" + +[package.dependencies] +six = "*" +SQLAlchemy = ">=1.0" + +[package.extras] +arrow = ["arrow (>=0.3.4)"] +babel = ["Babel (>=1.3)"] +color = ["colour (>=0.0.4)"] +encrypted = ["cryptography (>=0.6)"] +intervals = ["intervals (>=0.7.1)"] +password = ["passlib (>=1.6,<2.0)"] +pendulum = ["pendulum (>=2.0.5)"] +phone = ["phonenumbers (>=5.9.2)"] +test = ["pytest (>=2.7.1)", "Pygments (>=1.2)", "Jinja2 (>=2.3)", "docutils (>=0.10)", "flexmock (>=0.9.7)", "mock (==2.0.0)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pg8000 (>=1.12.4)", "pytz (>=2014.2)", "python-dateutil (>=2.6)", "pymysql", "flake8 (>=2.4.0)", "isort (>=4.2.2)", "pyodbc", "backports.zoneinfo"] +test_all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "mock (==2.0.0)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)", "backports.zoneinfo"] +timezone = ["python-dateutil"] +url = ["furl (>=0.4.1)"] + +[[package]] +name = "sqlalchemy2-stubs" +version = "0.0.2a20" +description = "Typing Stubs for SQLAlchemy 1.4" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = ">=3.7.4" + +[[package]] +name = "starlette" +version = "0.17.1" +description = "The little ASGI library that shines." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +anyio = ">=3.0.0,<4" + +[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" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "types-passlib" +version = "1.7.0" +description = "Typing stubs for passlib" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "typing-extensions" +version = "4.1.1" +description = "Backported and Experimental Type Hints for Python 3.6+" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "urllib3" +version = "1.26.8" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "uvicorn" +version = "0.15.0" +description = "The lightning-fast ASGI server." +category = "main" +optional = false +python-versions = "*" + +[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\""} +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\""} + +[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)"] + +[[package]] +name = "uvloop" +version = "0.16.0" +description = "Fast implementation of asyncio event loop on top of libuv" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"] +test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] + +[[package]] +name = "watchgod" +version = "0.7" +description = "Simple, modern file watching and code reload in python." +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "websockets" +version = "10.2" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "wrapt" +version = "1.13.3" +description = "Module for decorators, wrappers and monkey patching." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[metadata] +lock-version = "1.1" +python-versions = "^3.10" +content-hash = "3403f5ed73002034af083ee5589dc4828ad5dcc2cfc04a467d02dece886e19e1" + +[metadata.files] +alembic = [ + {file = "alembic-1.7.6-py3-none-any.whl", hash = "sha256:ad842f2c3ab5c5d4861232730779c05e33db4ba880a08b85eb505e87c01095bc"}, + {file = "alembic-1.7.6.tar.gz", hash = "sha256:6c0c05e9768a896d804387e20b299880fe01bc56484246b0dffe8075d6d3d847"}, +] +anyio = [ + {file = "anyio-3.5.0-py3-none-any.whl", hash = "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e"}, + {file = "anyio-3.5.0.tar.gz", hash = "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6"}, +] +asgiref = [ + {file = "asgiref-3.5.0-py3-none-any.whl", hash = "sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9"}, + {file = "asgiref-3.5.0.tar.gz", hash = "sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0"}, +] +astroid = [ + {file = "astroid-2.9.3-py3-none-any.whl", hash = "sha256:506daabe5edffb7e696ad82483ad0228245a9742ed7d2d8c9cdb31537decf9f6"}, + {file = "astroid-2.9.3.tar.gz", hash = "sha256:1efdf4e867d4d8ba4a9f6cf9ce07cd182c4c41de77f23814feb27ca93ca9d877"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {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"}, +] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +cffi = [ + {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, + {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, + {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, + {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, + {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, + {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, + {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, + {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, + {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, + {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, + {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, + {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, + {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, + {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, + {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, + {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, + {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {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"}, +] +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"}, +] +cryptography = [ + {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b"}, + {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2"}, + {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f"}, + {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3"}, + {file = "cryptography-36.0.1-cp36-abi3-win32.whl", hash = "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca"}, + {file = "cryptography-36.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"}, + {file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"}, +] +ecdsa = [ + {file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"}, + {file = "ecdsa-0.17.0.tar.gz", hash = "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"}, +] +environs = [ + {file = "environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124"}, + {file = "environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"}, +] +fastapi = [ + {file = "fastapi-0.74.1-py3-none-any.whl", hash = "sha256:b8ec8400623ef0b2ff558ebe06753b349f8e3a5dd38afea650800f2644ddba34"}, + {file = "fastapi-0.74.1.tar.gz", hash = "sha256:b58a2c46df14f62ebe6f24a9439927539ba1959b9be55ba0e2f516a683e5b9d4"}, +] +greenlet = [ + {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, + {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, + {file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"}, + {file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"}, + {file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"}, + {file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"}, + {file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"}, + {file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, + {file = "greenlet-1.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965"}, + {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, + {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, + {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, + {file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"}, + {file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"}, + {file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"}, + {file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, + {file = "greenlet-1.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f"}, + {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, + {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, + {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, + {file = "greenlet-1.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe"}, + {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, + {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, + {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, + {file = "greenlet-1.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2"}, + {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, + {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, + {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, + {file = "greenlet-1.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3"}, + {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, + {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, + {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"}, +] +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"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] +lazy-object-proxy = [ + {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, + {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, +] +mako = [ + {file = "Mako-1.2.0-py3-none-any.whl", hash = "sha256:23aab11fdbbb0f1051b93793a58323ff937e98e34aece1c4219675122e57e4ba"}, + {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"}, +] +markupsafe = [ + {file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-win32.whl", hash = "sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-win32.whl", hash = "sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-win32.whl", hash = "sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-win32.whl", hash = "sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7"}, + {file = "MarkupSafe-2.1.0.tar.gz", hash = "sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f"}, +] +marshmallow = [ + {file = "marshmallow-3.15.0-py3-none-any.whl", hash = "sha256:ff79885ed43b579782f48c251d262e062bce49c65c52412458769a4fb57ac30f"}, + {file = "marshmallow-3.15.0.tar.gz", hash = "sha256:2aaaab4f01ef4f5a011a21319af9fce17ab13bf28a026d1252adab0e035648d5"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +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"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +passlib = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {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"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pyasn1 = [ + {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, + {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, + {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, + {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, + {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, + {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, + {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, + {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, + {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, + {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, + {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, + {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, +] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] +pydantic = [ + {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"}, + {file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"}, + {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37"}, + {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25"}, + {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6"}, + {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c"}, + {file = "pydantic-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398"}, + {file = "pydantic-1.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65"}, + {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46"}, + {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c"}, + {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054"}, + {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed"}, + {file = "pydantic-1.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1"}, + {file = "pydantic-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070"}, + {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2"}, + {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1"}, + {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032"}, + {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6"}, + {file = "pydantic-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d"}, + {file = "pydantic-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7"}, + {file = "pydantic-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77"}, + {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9"}, + {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6"}, + {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145"}, + {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034"}, + {file = "pydantic-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f"}, + {file = "pydantic-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b"}, + {file = "pydantic-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c"}, + {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce"}, + {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3"}, + {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d"}, + {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721"}, + {file = "pydantic-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16"}, + {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"}, + {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"}, +] +pylint = [ + {file = "pylint-2.12.2-py3-none-any.whl", hash = "sha256:daabda3f7ed9d1c60f52d563b1b854632fd90035bcf01443e234d3dc794e3b74"}, + {file = "pylint-2.12.2.tar.gz", hash = "sha256:9d945a73640e1fec07ee34b42f5669b770c759acd536ec7b16d7e4b87a9c9ff9"}, +] +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"}, +] +pytest = [ + {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, + {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, +] +pytest-cov = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] +pytest-env = [ + {file = "pytest-env-0.6.2.tar.gz", hash = "sha256:7e94956aef7f2764f3c147d216ce066bf6c42948bb9e293169b1b1c880a580c2"}, +] +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"}, +] +python-jose = [ + {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, + {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, +] +python-multipart = [ + {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, +] +pyyaml = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +requests = [ + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, +] +rsa = [ + {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, + {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +sniffio = [ + {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, + {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"}, +] +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"}, +] +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"}, +] +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"}, +] +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"}, +] +urllib3 = [ + {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, + {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, +] +uvicorn = [ + {file = "uvicorn-0.15.0-py3-none-any.whl", hash = "sha256:17f898c64c71a2640514d4089da2689e5db1ce5d4086c2d53699bf99513421c1"}, + {file = "uvicorn-0.15.0.tar.gz", hash = "sha256:d9a3c0dd1ca86728d3e235182683b4cf94cd53a867c288eaeca80ee781b2caff"}, +] +uvloop = [ + {file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"}, + {file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c"}, + {file = "uvloop-0.16.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64"}, + {file = "uvloop-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9"}, + {file = "uvloop-0.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638"}, + {file = "uvloop-0.16.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450"}, + {file = "uvloop-0.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805"}, + {file = "uvloop-0.16.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382"}, + {file = "uvloop-0.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee"}, + {file = "uvloop-0.16.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464"}, + {file = "uvloop-0.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab"}, + {file = "uvloop-0.16.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f"}, + {file = "uvloop-0.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897"}, + {file = "uvloop-0.16.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f"}, + {file = "uvloop-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861"}, + {file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"}, +] +watchgod = [ + {file = "watchgod-0.7-py3-none-any.whl", hash = "sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7"}, + {file = "watchgod-0.7.tar.gz", hash = "sha256:48140d62b0ebe9dd9cf8381337f06351e1f2e70b2203fa9c6eff4e572ca84f29"}, +] +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"}, +] +wrapt = [ + {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, + {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, + {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, + {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, + {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, + {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, + {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, + {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, + {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, + {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, + {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, + {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, + {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, + {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, + {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, + {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, + {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, + {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, + {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, +] From 13c36690c02ae693d8fc8f10bcf8ae0ed32f8ec8 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 14 Mar 2022 00:12:06 +0100 Subject: [PATCH 008/536] Add coverage to pyproject --- backend/.coveragerc | 8 ------- backend/poetry.lock | 2 +- backend/pyproject.toml | 47 ++++++++++++++++++++++++++---------------- 3 files changed, 30 insertions(+), 27 deletions(-) delete mode 100644 backend/.coveragerc diff --git a/backend/.coveragerc b/backend/.coveragerc deleted file mode 100644 index 03cd94de9..000000000 --- a/backend/.coveragerc +++ /dev/null @@ -1,8 +0,0 @@ -[run] -omit = - *tests* - -[report] -omit = - *tests* - *__init__.py* \ No newline at end of file diff --git a/backend/poetry.lock b/backend/poetry.lock index 53864c1ef..8912919c5 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -837,7 +837,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "3403f5ed73002034af083ee5589dc4828ad5dcc2cfc04a467d02dece886e19e1" +content-hash = "dc542c7c2942538b6b133e26650b0f089c52ee22083b86b639d354635f05ad1d" [metadata.files] alembic = [ diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 52123eca2..a14d83488 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -9,76 +9,87 @@ license = "MIT" python = "^3.10" # Alembic: Database migrations extension for SQLAlchemy -alembic="1.7.6" +alembic = "1.7.6" # Environs: simplified environment variable parsing -environs="9.5.0" +environs = "9.5.0" # FastAPI: API framework -fastapi="0.74.1" +fastapi = "0.74.1" # MariaDB: Python MariaDB connector -mariadb="1.0.10" +mariadb = "1.0.10" # Hash passwords -passlib = { "version" = "1.7.4", extras = ["bcrypt"]} +passlib = { "version" = "1.7.4", extras = ["bcrypt"] } # Generate and verify JWT tokens 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.5.3" # OAuth2 form data -python-multipart="0.0.5" +python-multipart = "0.0.5" # Requests: HTTP library -requests="2.27.1" +requests = "2.27.1" # SQLAlchemy: ORM and database toolkit -SQLAlchemy="1.4.31" +SQLAlchemy = "1.4.31" # SQLAlchemy-Utils: Various utility functions and datatypes for SQLAlchemy -sqlalchemy-utils="0.38.2" +sqlalchemy-utils = "0.38.2" # Uvicorn: ASGI web server implementation uvicorn = { "version" = ">=0.12.0, < 0.16.0", extras = ["standard"] } [tool.poetry.dev-dependencies] # Coverage: generate code coverage reports -coverage="6.3.1" +coverage = { "version" = "6.3.1", extras = ["toml"] } # faker: Generate dummy data faker = "13.3.1" # Mypy: check type usage in code -mypy="0.940" +mypy = "0.940" # Pylint: Python linter -pylint="2.12.2" +pylint = "2.12.2" # Pylint-Pytest: A Pylint plugin to suppress pytest-related false positives. -pylint-pytest="1.1.2" +pylint-pytest = "1.1.2" # Pytest: Python testing framework # (more advanced than the built-in unittest module) -pytest="7.0.1" +pytest = "7.0.1" # Pytest-cov: coverage plugin for pytest -pytest-cov="3.0.0" +pytest-cov = "3.0.0" # Pytest-env: env plugin for pytest -pytest-env="0.6.2" +pytest-env = "0.6.2" # Pytest-mock: mocking for pytest pytest-mock = "3.7.0" # Sqlalchemy-stubs: type hints for sqlalchemy -sqlalchemy2-stubs="0.0.2a20" +sqlalchemy2-stubs = "0.0.2a20" # Types for the passlib library types-passlib = "1.7.0" +[tool.coverage.run] +omit = [ + "*tests*", +] + +[tool.coverage.report] +omit = [ + "*tests*", + "*__init__.py*", +] + [tool.mypy] plugins = ["sqlalchemy.ext.mypy.plugin"] From f12c5db92d850c8840c8e7e292a53bacfa76aebc Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 14 Mar 2022 00:25:29 +0100 Subject: [PATCH 009/536] Update project setup information --- backend/README.md | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/backend/README.md b/backend/README.md index 776b940e4..7f354e85b 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,8 +1,11 @@ # Backend -## Setting up a venv +## Setting up a venv and installing dependencies ```bash +# Install Poetry +pip3 install poetry + # Navigate to this directory cd backend @@ -13,11 +16,11 @@ python3 -m venv venv # PyCharm does this automatically, so this is only required if you're using another IDE source venv/bin/activate -# Install requirements -pip3 install -r requirements.txt +# Alternatively: activate the venv using Poetry +poetry shell -# Install dev requirements -pip3 install -r requirements-dev.txt +# Install all dependencies and dev dependencies +poetry install ``` Note that, in case your IDE does not do this automatically, you have to run `source venv/bin/activate` every time you want to run the backend, as otherwise your interpreter won't be able to find the packages. @@ -34,9 +37,15 @@ This directory contains a `.env.example` file which shows the general structure ## Keeping requirements up to date -Whenever you'd like to install a new package, make sure to **update the `requirements.txt` or `requirements-dev.txt` files** so that everyone has the same packages installed, and our tests can run easily. +Whenever you'd like to install a new package, install it using `Poetry` so that the [`pyproject.toml`](pyproject.toml)-file is always up-to-date. This ensures we always have the same versions of every package. + +```shell +# Install a regular dependency +poetry add package_name -In case your package installs multiple other dependencies, it's not necessary to install those along with it. The main package you installed (along with the correct version) is satisfactory. +# Install a dev dependency +poetry add --dev package_name +``` ## Type annotations From 12ffdd0957653aa54352a6bd25b65f7a4089fbb2 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 14 Mar 2022 11:40:39 +0100 Subject: [PATCH 010/536] Update poetry install instructions --- backend/README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/README.md b/backend/README.md index 7f354e85b..2748a346a 100644 --- a/backend/README.md +++ b/backend/README.md @@ -2,10 +2,19 @@ ## Setting up a venv and installing dependencies -```bash -# Install Poetry -pip3 install poetry +First, Install Poetry: +```shell +# Linux & MacOS +curl -sSL https://install.python-poetry.org | python3 - + +# Windows (Powershell) +(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python - +``` + +Next, create a venv and install the dependencies: + +```shell # Navigate to this directory cd backend From 8f796e8bd93af69ae2f2ddcac37b8c25698fa821 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 14 Mar 2022 18:10:40 +0100 Subject: [PATCH 011/536] try to fix action --- .github/workflows/backend.yml | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 4f3635ca0..8e896c1e4 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -17,22 +17,23 @@ jobs: steps: - run: apk add --no-cache tar - uses: actions/checkout@v2 + - uses: abatilo/actions-poetry@v2.0.0 - uses: actions/cache@v2 id: cache with: - path: /usr/local/lib/python3.10/site-packages - key: ${{ runner.os }}-site-packages-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }} + path: ~/.virtualenvs + key: poetry-${{ hashFiles('**/poetry.lock') }} restore-keys: | - ${{ runner.os }}-site-packages- + poetry-${{ hashFiles('**/poetry.lock') }} + - run: | + poetry config settings.virtualenvs.in-project false + poetry config settings.virtualenvs.path ~/.virtualenvs - if: steps.cache.outputs.cache-hit != 'true' run: apk add gcc musl-dev build-base mariadb-dev libffi-dev - if: steps.cache.outputs.cache-hit != 'true' - run: pip install -r requirements.txt - - - if: steps.cache.outputs.cache-hit != 'true' - run: pip install -r requirements-dev.txt + run: poetry install Test: needs: [Dependencies] @@ -43,10 +44,10 @@ jobs: - uses: actions/checkout@v2 - uses: actions/cache@v2 with: - path: /usr/local/lib/python3.10/site-packages - key: ${{ runner.os }}-site-packages-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }} + path: ~/.virtualenvs + key: poetry-${{ hashFiles('**/poetry.lock') }} - - run: python -m pytest + - run: poetry run pytest Lint: needs: [Test] @@ -57,10 +58,10 @@ jobs: - uses: actions/checkout@v2 - uses: actions/cache@v2 with: - path: /usr/local/lib/python3.10/site-packages - key: ${{ runner.os }}-site-packages-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }} + path: ~/.virtualenvs + key: poetry-${{ hashFiles('**/poetry.lock') }} - - run: python -m pylint src tests + - run: poetry run pylint src tests Type: needs: [Test] @@ -71,7 +72,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/cache@v2 with: - path: /usr/local/lib/python3.10/site-packages - key: ${{ runner.os }}-site-packages-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }} + path: ~/.virtualenvs + key: poetry-${{ hashFiles('**/poetry.lock') }} - - run: python -m mypy src tests + - run: poetry run mypy src tests From 7ad991701aeb5f440b142563855f56dcb64e9d39 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 14 Mar 2022 18:23:47 +0100 Subject: [PATCH 012/536] try to fix action --- .github/workflows/backend.yml | 40 ++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 8e896c1e4..745e3d7bf 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -17,23 +17,23 @@ jobs: steps: - run: apk add --no-cache tar - uses: actions/checkout@v2 - - uses: abatilo/actions-poetry@v2.0.0 + - uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true - uses: actions/cache@v2 id: cache with: - path: ~/.virtualenvs - key: poetry-${{ hashFiles('**/poetry.lock') }} + path: .venv + key: venv-${{ runner.os }}-3.10-${{ hashFiles('**/poetry.lock') }} restore-keys: | - poetry-${{ hashFiles('**/poetry.lock') }} - - run: | - poetry config settings.virtualenvs.in-project false - poetry config settings.virtualenvs.path ~/.virtualenvs + venv-${{ runner.os }}- - if: steps.cache.outputs.cache-hit != 'true' run: apk add gcc musl-dev build-base mariadb-dev libffi-dev - if: steps.cache.outputs.cache-hit != 'true' - run: poetry install + run: poetry install --no-interaction Test: needs: [Dependencies] @@ -44,10 +44,12 @@ jobs: - uses: actions/checkout@v2 - uses: actions/cache@v2 with: - path: ~/.virtualenvs - key: poetry-${{ hashFiles('**/poetry.lock') }} + path: .venv + key: venv-${{ runner.os }}-3.10-${{ hashFiles('**/poetry.lock') }} - - run: poetry run pytest + - run: | + source .venv/bin/activate + poetry run pytest Lint: needs: [Test] @@ -58,10 +60,12 @@ jobs: - uses: actions/checkout@v2 - uses: actions/cache@v2 with: - path: ~/.virtualenvs - key: poetry-${{ hashFiles('**/poetry.lock') }} + path: .venv + key: venv-${{ runner.os }}-3.10-${{ hashFiles('**/poetry.lock') }} - - run: poetry run pylint src tests + - run: | + source .venv/bin/activate + poetry run pylint src tests Type: needs: [Test] @@ -72,7 +76,9 @@ jobs: - uses: actions/checkout@v2 - uses: actions/cache@v2 with: - path: ~/.virtualenvs - key: poetry-${{ hashFiles('**/poetry.lock') }} + path: .venv + key: venv-${{ runner.os }}-3.10-${{ hashFiles('**/poetry.lock') }} - - run: poetry run mypy src tests + - run: | + source .venv/bin/activate + poetry run mypy src tests From 1de4c11c16cd22f88ade28af05cd6db6b4f660dc Mon Sep 17 00:00:00 2001 From: stijndcl Date: Tue, 15 Mar 2022 14:08:38 +0100 Subject: [PATCH 013/536] install bash --- .github/workflows/backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 745e3d7bf..35c9a30f3 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -15,7 +15,7 @@ jobs: runs-on: self-hosted container: python:3.10.2-alpine steps: - - run: apk add --no-cache tar + - run: apk add --no-cache tar bash - uses: actions/checkout@v2 - uses: snok/install-poetry@v1 with: From 0ccb643f04d7ef0cc9f2e0df9eb90a03ff04dc15 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Tue, 15 Mar 2022 14:10:17 +0100 Subject: [PATCH 014/536] poetry install via bash --- .github/workflows/backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 35c9a30f3..14b405ca8 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -33,7 +33,7 @@ jobs: run: apk add gcc musl-dev build-base mariadb-dev libffi-dev - if: steps.cache.outputs.cache-hit != 'true' - run: poetry install --no-interaction + run: bash -c "poetry install --no-interaction" Test: needs: [Dependencies] From fc76900c13ada5da5946322ccd7deea7e25c091c Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 08:22:09 +0100 Subject: [PATCH 015/536] install poetry via pip --- .github/workflows/backend.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 14b405ca8..47e1286bd 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -15,16 +15,13 @@ jobs: runs-on: self-hosted container: python:3.10.2-alpine steps: - - run: apk add --no-cache tar bash + - run: apk add --no-cache tar - uses: actions/checkout@v2 - - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: true + - uses: actions/cache@v2 id: cache with: - path: .venv + path: /usr/local/lib/python3.10/site-packages key: venv-${{ runner.os }}-3.10-${{ hashFiles('**/poetry.lock') }} restore-keys: | venv-${{ runner.os }}- @@ -33,7 +30,10 @@ jobs: run: apk add gcc musl-dev build-base mariadb-dev libffi-dev - if: steps.cache.outputs.cache-hit != 'true' - run: bash -c "poetry install --no-interaction" + run: pip install poetry + + - if: steps.cache.outputs.cache-hit != 'true' + run: POETRY_VIRTUALENVS_CREATE=false python -m poetry install -n Test: needs: [Dependencies] From a2bfc38a72777d627eb53dc67fca91924876c218 Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 08:26:31 +0100 Subject: [PATCH 016/536] update pip --- .github/workflows/backend.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 47e1286bd..d136cc69f 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -30,7 +30,9 @@ jobs: run: apk add gcc musl-dev build-base mariadb-dev libffi-dev - if: steps.cache.outputs.cache-hit != 'true' - run: pip install poetry + run: | + pip install -U pip + pip install poetry - if: steps.cache.outputs.cache-hit != 'true' run: POETRY_VIRTUALENVS_CREATE=false python -m poetry install -n From 705e287345ad387c1f21b8638dc74d63440cccc4 Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 08:31:56 +0100 Subject: [PATCH 017/536] add rust toolchain :( --- .github/workflows/backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index d136cc69f..6b20b81b7 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -27,7 +27,7 @@ jobs: venv-${{ runner.os }}- - if: steps.cache.outputs.cache-hit != 'true' - run: apk add gcc musl-dev build-base mariadb-dev libffi-dev + run: apk add gcc musl-dev build-base mariadb-dev libffi-dev cargo - if: steps.cache.outputs.cache-hit != 'true' run: | From f1c8edc013d8eed7dcde892532a1c8be801ded48 Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 08:38:41 +0100 Subject: [PATCH 018/536] ditch alpine - builds too slow --- .github/workflows/backend.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 6b20b81b7..e1b0a102f 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -13,9 +13,10 @@ defaults: jobs: Dependencies: runs-on: self-hosted - container: python:3.10.2-alpine + container: python:3.10.2-slim-bullseye steps: - - run: apk add --no-cache tar + #- run: apk add --no-cache tar + - uses: actions/checkout@v2 - uses: actions/cache@v2 @@ -26,8 +27,8 @@ jobs: restore-keys: | venv-${{ runner.os }}- - - if: steps.cache.outputs.cache-hit != 'true' - run: apk add gcc musl-dev build-base mariadb-dev libffi-dev cargo + #- if: steps.cache.outputs.cache-hit != 'true' + # run: apk add gcc musl-dev build-base mariadb-dev libffi-dev cargo - if: steps.cache.outputs.cache-hit != 'true' run: | @@ -40,7 +41,7 @@ jobs: Test: needs: [Dependencies] runs-on: self-hosted - container: python:3.10.2-alpine + container: python:3.10.2-slim-bullseye steps: - run: apk add --no-cache tar - uses: actions/checkout@v2 @@ -56,7 +57,7 @@ jobs: Lint: needs: [Test] runs-on: self-hosted - container: python:3.10.2-alpine + container: python:3.10.2-slim-bullseye steps: - run: apk add --no-cache tar - uses: actions/checkout@v2 @@ -72,7 +73,7 @@ jobs: Type: needs: [Test] runs-on: self-hosted - container: python:3.10.2-alpine + container: python:3.10.2-slim-bullseye steps: - run: apk add --no-cache tar - uses: actions/checkout@v2 From 7c7a7d6b73b3862ddd104a16aed6f195c493127c Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 08:41:54 +0100 Subject: [PATCH 019/536] use other cache --- .github/workflows/backend.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index e1b0a102f..5a74386e1 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -23,9 +23,9 @@ jobs: id: cache with: path: /usr/local/lib/python3.10/site-packages - key: venv-${{ runner.os }}-3.10-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} restore-keys: | - venv-${{ runner.os }}- + venv-${{ runner.os }}-bullseye-3.10- #- if: steps.cache.outputs.cache-hit != 'true' # run: apk add gcc musl-dev build-base mariadb-dev libffi-dev cargo From d3ce817798835bfb7dbf7499c2907d6d9d7eadc4 Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 08:44:27 +0100 Subject: [PATCH 020/536] add build-essentials --- .github/workflows/backend.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 5a74386e1..e0f3c6731 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -32,6 +32,8 @@ jobs: - if: steps.cache.outputs.cache-hit != 'true' run: | + apt update + apt install -y build-essential pip install -U pip pip install poetry From e8df413c594b26d24906fd311e4e896328204153 Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 08:48:32 +0100 Subject: [PATCH 021/536] add mariadb client --- .github/workflows/backend.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index e0f3c6731..f89040e1e 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -31,11 +31,13 @@ jobs: # run: apk add gcc musl-dev build-base mariadb-dev libffi-dev cargo - if: steps.cache.outputs.cache-hit != 'true' - run: | - apt update - apt install -y build-essential - pip install -U pip - pip install poetry + run: apt update + + - if: steps.cache.outputs.cache-hit != 'true' + run: apt install -y build-essential mariadb-client + + - if: steps.cache.outputs.cache-hit != 'true' + run: pip install -U pip poetry - if: steps.cache.outputs.cache-hit != 'true' run: POETRY_VIRTUALENVS_CREATE=false python -m poetry install -n From 9295c4deac56df0d0556f74038a510c4a5baa7b1 Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 08:50:48 +0100 Subject: [PATCH 022/536] install libmariadb-dev instead --- .github/workflows/backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index f89040e1e..2e92e9120 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -34,7 +34,7 @@ jobs: run: apt update - if: steps.cache.outputs.cache-hit != 'true' - run: apt install -y build-essential mariadb-client + run: apt install -y build-essential libmariadb-dev - if: steps.cache.outputs.cache-hit != 'true' run: pip install -U pip poetry From 97948bdb2edabdce0b953209d758ee4725c20abe Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 08:54:24 +0100 Subject: [PATCH 023/536] remove tar & venv --- .github/workflows/backend.yml | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 2e92e9120..dd4e6c8b8 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -47,45 +47,39 @@ jobs: runs-on: self-hosted container: python:3.10.2-slim-bullseye steps: - - run: apk add --no-cache tar + #- run: apk add --no-cache tar - uses: actions/checkout@v2 - uses: actions/cache@v2 with: path: .venv - key: venv-${{ runner.os }}-3.10-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} - - run: | - source .venv/bin/activate - poetry run pytest + - run: poetry run pytest Lint: needs: [Test] runs-on: self-hosted container: python:3.10.2-slim-bullseye steps: - - run: apk add --no-cache tar + #- run: apk add --no-cache tar - uses: actions/checkout@v2 - uses: actions/cache@v2 with: path: .venv - key: venv-${{ runner.os }}-3.10-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} - - run: | - source .venv/bin/activate - poetry run pylint src tests + - run: poetry run pylint src tests Type: needs: [Test] runs-on: self-hosted container: python:3.10.2-slim-bullseye steps: - - run: apk add --no-cache tar + #- run: apk add --no-cache tar - uses: actions/checkout@v2 - uses: actions/cache@v2 with: path: .venv - key: venv-${{ runner.os }}-3.10-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} - - run: | - source .venv/bin/activate - poetry run mypy src tests + - run: poetry run mypy src tests From ca7c32f2feab8d337e5b840e5b3f1f474346494b Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 08:57:33 +0100 Subject: [PATCH 024/536] add pip list --- .github/workflows/backend.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index dd4e6c8b8..b54465f81 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -54,6 +54,7 @@ jobs: path: .venv key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} + - run: pip list -v - run: poetry run pytest Lint: From a07a00cefbfe79ddfd5aef384a19b9362b4a6535 Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 08:59:28 +0100 Subject: [PATCH 025/536] set correct cache path --- .github/workflows/backend.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index b54465f81..ba923ddea 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -51,7 +51,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/cache@v2 with: - path: .venv + path: /usr/local/lib/python3.10/site-packages key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} - run: pip list -v @@ -66,7 +66,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/cache@v2 with: - path: .venv + path: /usr/local/lib/python3.10/site-packages key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} - run: poetry run pylint src tests @@ -80,7 +80,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/cache@v2 with: - path: .venv + path: /usr/local/lib/python3.10/site-packages key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} - run: poetry run mypy src tests From c9480f0c8f48fda2056256128487d87d679a3bb3 Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 09:02:00 +0100 Subject: [PATCH 026/536] add python -m --- .github/workflows/backend.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index ba923ddea..933b4dd90 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -55,7 +55,7 @@ jobs: key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} - run: pip list -v - - run: poetry run pytest + - run: python -m poetry run pytest Lint: needs: [Test] @@ -69,7 +69,7 @@ jobs: path: /usr/local/lib/python3.10/site-packages key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} - - run: poetry run pylint src tests + - run: python -m poetry run pylint src tests Type: needs: [Test] @@ -83,4 +83,4 @@ jobs: path: /usr/local/lib/python3.10/site-packages key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} - - run: poetry run mypy src tests + - run: python -m poetry run mypy src tests From 708a133c68d32f44e7f315bead826619ad36ca8f Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 09:04:42 +0100 Subject: [PATCH 027/536] dont use virtualenv --- .github/workflows/backend.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 933b4dd90..38c637414 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -10,6 +10,9 @@ defaults: run: working-directory: backend +env: + POETRY_VIRTUALENVS_CREATE: false + jobs: Dependencies: runs-on: self-hosted @@ -40,7 +43,7 @@ jobs: run: pip install -U pip poetry - if: steps.cache.outputs.cache-hit != 'true' - run: POETRY_VIRTUALENVS_CREATE=false python -m poetry install -n + run: python -m poetry install -n Test: needs: [Dependencies] From 1f0ed0ad96c475c6c4c46f40385a62e78ebc7994 Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 09:08:55 +0100 Subject: [PATCH 028/536] yeet poetry run since we installed system wide --- .github/workflows/backend.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 38c637414..ab69f2833 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -58,7 +58,7 @@ jobs: key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} - run: pip list -v - - run: python -m poetry run pytest + - run: python -m pytest Lint: needs: [Test] @@ -72,7 +72,7 @@ jobs: path: /usr/local/lib/python3.10/site-packages key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} - - run: python -m poetry run pylint src tests + - run: python -m pylint src tests Type: needs: [Test] @@ -86,4 +86,4 @@ jobs: path: /usr/local/lib/python3.10/site-packages key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} - - run: python -m poetry run mypy src tests + - run: python -m mypy src tests From b35daef4496690880a0d8794837f3275ab7b312f Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 09:12:53 +0100 Subject: [PATCH 029/536] cleanup workflow --- .github/workflows/backend.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index ab69f2833..581e1ed82 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -18,10 +18,7 @@ jobs: runs-on: self-hosted container: python:3.10.2-slim-bullseye steps: - #- run: apk add --no-cache tar - - uses: actions/checkout@v2 - - uses: actions/cache@v2 id: cache with: @@ -30,9 +27,6 @@ jobs: restore-keys: | venv-${{ runner.os }}-bullseye-3.10- - #- if: steps.cache.outputs.cache-hit != 'true' - # run: apk add gcc musl-dev build-base mariadb-dev libffi-dev cargo - - if: steps.cache.outputs.cache-hit != 'true' run: apt update @@ -50,14 +44,12 @@ jobs: runs-on: self-hosted container: python:3.10.2-slim-bullseye steps: - #- run: apk add --no-cache tar - uses: actions/checkout@v2 - uses: actions/cache@v2 with: path: /usr/local/lib/python3.10/site-packages key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} - - run: pip list -v - run: python -m pytest Lint: @@ -65,7 +57,6 @@ jobs: runs-on: self-hosted container: python:3.10.2-slim-bullseye steps: - #- run: apk add --no-cache tar - uses: actions/checkout@v2 - uses: actions/cache@v2 with: @@ -79,7 +70,6 @@ jobs: runs-on: self-hosted container: python:3.10.2-slim-bullseye steps: - #- run: apk add --no-cache tar - uses: actions/checkout@v2 - uses: actions/cache@v2 with: From 72ae63582477908f0351d17661e210a038552e6e Mon Sep 17 00:00:00 2001 From: stijndcl Date: Thu, 17 Mar 2022 10:05:30 +0100 Subject: [PATCH 030/536] Update readme --- backend/README.md | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/backend/README.md b/backend/README.md index 2748a346a..0f7a5a341 100644 --- a/backend/README.md +++ b/backend/README.md @@ -2,17 +2,7 @@ ## Setting up a venv and installing dependencies -First, Install Poetry: - -```shell -# Linux & MacOS -curl -sSL https://install.python-poetry.org | python3 - - -# Windows (Powershell) -(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python - -``` - -Next, create a venv and install the dependencies: +Create a venv, install Poetry, and then install the dependencies: ```shell # Navigate to this directory @@ -25,8 +15,8 @@ python3 -m venv venv # PyCharm does this automatically, so this is only required if you're using another IDE source venv/bin/activate -# Alternatively: activate the venv using Poetry -poetry shell +# Install Poetry +pip3 install poetry # Install all dependencies and dev dependencies poetry install From c842a2a36eb37d1b215d0d5b44ecd7bca528498d Mon Sep 17 00:00:00 2001 From: stijndcl Date: Thu, 17 Mar 2022 10:34:19 +0100 Subject: [PATCH 031/536] Update dependencies --- backend/poetry.lock | 205 +++++++++++++++++++++++++++-------------- backend/pyproject.toml | 22 +---- 2 files changed, 137 insertions(+), 90 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index 8912919c5..7ecdcb9c2 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -156,7 +156,7 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "36.0.1" +version = "36.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -206,6 +206,17 @@ django = ["dj-database-url", "dj-email-url", "django-cache-url"] lint = ["flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)"] tests = ["pytest", "dj-database-url", "dj-email-url", "django-cache-url"] +[[package]] +name = "faker" +version = "13.3.1" +description = "Faker is a Python package that generates fake data for you." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +python-dateutil = ">=2.4" + [[package]] name = "fastapi" version = "0.74.1" @@ -318,7 +329,7 @@ python-versions = ">=3.6" [[package]] name = "markupsafe" -version = "2.1.0" +version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false @@ -466,6 +477,14 @@ typing-extensions = ">=3.7.4.3" dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] +[[package]] +name = "pyhumps" +version = "3.5.3" +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 +python-versions = "*" + [[package]] name = "pylint" version = "2.12.2" @@ -552,6 +571,31 @@ python-versions = "*" [package.dependencies] pytest = ">=2.6.0" +[[package]] +name = "pytest-mock" +version = "3.7.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "tox", "pytest-asyncio"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "0.19.2" @@ -763,14 +807,14 @@ python-versions = ">=3.6" [[package]] name = "urllib3" -version = "1.26.8" +version = "1.26.9" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] -brotli = ["brotlipy (>=0.6.0)"] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] @@ -812,11 +856,14 @@ test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,< [[package]] name = "watchgod" -version = "0.7" +version = "0.8" description = "Simple, modern file watching and code reload in python." category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" + +[package.dependencies] +anyio = ">=3.0.0,<4" [[package]] name = "websockets" @@ -837,7 +884,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "dc542c7c2942538b6b133e26650b0f089c52ee22083b86b639d354635f05ad1d" +content-hash = "5bf64e89fc7b51ce108190ab5525229fc27a35b889808d5507c3d2f22f5de71d" [metadata.files] alembic = [ @@ -988,26 +1035,26 @@ coverage = [ {file = "coverage-6.3.1.tar.gz", hash = "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8"}, ] cryptography = [ - {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b"}, - {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2"}, - {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f"}, - {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3"}, - {file = "cryptography-36.0.1-cp36-abi3-win32.whl", hash = "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca"}, - {file = "cryptography-36.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf"}, - {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9"}, - {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1"}, - {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"}, - {file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"}, + {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"}, ] ecdsa = [ {file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"}, @@ -1017,6 +1064,10 @@ environs = [ {file = "environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124"}, {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"}, +] fastapi = [ {file = "fastapi-0.74.1-py3-none-any.whl", hash = "sha256:b8ec8400623ef0b2ff558ebe06753b349f8e3a5dd38afea650800f2644ddba34"}, {file = "fastapi-0.74.1.tar.gz", hash = "sha256:b58a2c46df14f62ebe6f24a9439927539ba1959b9be55ba0e2f516a683e5b9d4"}, @@ -1166,46 +1217,46 @@ mariadb = [ {file = "mariadb-1.0.10.zip", hash = "sha256:79028ba6051173dad1ad0be7518389cab70239f92b4ff8b8813dae55c3f2c53d"}, ] markupsafe = [ - {file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-win32.whl", hash = "sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-win32.whl", hash = "sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-win32.whl", hash = "sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-win32.whl", hash = "sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7"}, - {file = "MarkupSafe-2.1.0.tar.gz", hash = "sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, + {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] marshmallow = [ {file = "marshmallow-3.15.0-py3-none-any.whl", hash = "sha256:ff79885ed43b579782f48c251d262e062bce49c65c52412458769a4fb57ac30f"}, @@ -1320,6 +1371,10 @@ pydantic = [ {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"}, {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"}, +] pylint = [ {file = "pylint-2.12.2-py3-none-any.whl", hash = "sha256:daabda3f7ed9d1c60f52d563b1b854632fd90035bcf01443e234d3dc794e3b74"}, {file = "pylint-2.12.2.tar.gz", hash = "sha256:9d945a73640e1fec07ee34b42f5669b770c759acd536ec7b16d7e4b87a9c9ff9"}, @@ -1342,6 +1397,14 @@ pytest-cov = [ pytest-env = [ {file = "pytest-env-0.6.2.tar.gz", hash = "sha256:7e94956aef7f2764f3c147d216ce066bf6c42948bb9e293169b1b1c880a580c2"}, ] +pytest-mock = [ + {file = "pytest-mock-3.7.0.tar.gz", hash = "sha256:5112bd92cc9f186ee96e1a92efc84969ea494939c3aead39c50f421c4cc69534"}, + {file = "pytest_mock-3.7.0-py3-none-any.whl", hash = "sha256:6cff27cec936bf81dc5ee87f07132b807bcda51106b5ec4b90a04331cba76231"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {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"}, @@ -1471,8 +1534,8 @@ typing-extensions = [ {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, ] urllib3 = [ - {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, - {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, + {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"}, @@ -1497,8 +1560,8 @@ uvloop = [ {file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"}, ] watchgod = [ - {file = "watchgod-0.7-py3-none-any.whl", hash = "sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7"}, - {file = "watchgod-0.7.tar.gz", hash = "sha256:48140d62b0ebe9dd9cf8381337f06351e1f2e70b2203fa9c6eff4e572ca84f29"}, + {file = "watchgod-0.8-py3-none-any.whl", hash = "sha256:339c2cfede1ccc1e277bbf5e82e42886f3c80801b01f45ab10d9461c4118b5eb"}, + {file = "watchgod-0.8.tar.gz", hash = "sha256:29a1d8f25e1721ddb73981652ca318c47387ffb12ec4171ddd7b9d01540033b1"}, ] websockets = [ {file = "websockets-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5396710f86a306cf52f87fd8ea594a0e894ba0cc5a36059eaca3a477dc332aa"}, diff --git a/backend/pyproject.toml b/backend/pyproject.toml index a14d83488..701ca6c05 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -68,27 +68,13 @@ pytest = "7.0.1" pytest-cov = "3.0.0" # Pytest-env: env plugin for pytest -pytest-env = "0.6.2" - -# Pytest-mock: mocking for pytest -pytest-mock = "3.7.0" +pytest-env="0.6.2" # Sqlalchemy-stubs: type hints for sqlalchemy -sqlalchemy2-stubs = "0.0.2a20" +sqlalchemy2-stubs="0.0.2a20" # Types for the passlib library -types-passlib = "1.7.0" - -[tool.coverage.run] -omit = [ - "*tests*", -] - -[tool.coverage.report] -omit = [ - "*tests*", - "*__init__.py*", -] +types-passlib="1.7.0" [tool.mypy] plugins = ["sqlalchemy.ext.mypy.plugin"] @@ -103,13 +89,11 @@ ignore_missing_imports = true [tool.pylint.master] load-plugins=["pylint_pytest"] -argument-rgx = "[a-z_][a-z0-9_]{1,31}$" disable=[ "import-outside-toplevel", "missing-module-docstring", "too-few-public-methods", ] -extension-pkg-whitelist = "pydantic" [tool.pylint.format] max-line-length = 120 From ad6a2b954cd27ff8637c876684959f805873181f Mon Sep 17 00:00:00 2001 From: stijndcl Date: Thu, 17 Mar 2022 10:49:09 +0100 Subject: [PATCH 032/536] Add missing dep --- backend/pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 701ca6c05..fe3aff296 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -68,7 +68,10 @@ pytest = "7.0.1" pytest-cov = "3.0.0" # Pytest-env: env plugin for pytest -pytest-env="0.6.2" +pytest-env = "0.6.2" + +# Pytest-mock: mocking library for pytest +pytest-mock = "3.7.0" # Sqlalchemy-stubs: type hints for sqlalchemy sqlalchemy2-stubs="0.0.2a20" From d3c60962f4384ea065c077d473e2fec4fe449868 Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 11:01:30 +0100 Subject: [PATCH 033/536] remove env section --- .github/workflows/backend.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 581e1ed82..c2d77d7aa 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -10,9 +10,6 @@ defaults: run: working-directory: backend -env: - POETRY_VIRTUALENVS_CREATE: false - jobs: Dependencies: runs-on: self-hosted @@ -37,7 +34,7 @@ jobs: run: pip install -U pip poetry - if: steps.cache.outputs.cache-hit != 'true' - run: python -m poetry install -n + run: POETRY_VIRTUALENVS_CREATE=false python -m poetry install -n Test: needs: [Dependencies] From 9389e69f42a2e2cb9248959273dfd01bdb3ed9a6 Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 11:02:59 +0100 Subject: [PATCH 034/536] add newline to trigger new install in actions --- backend/poetry.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/poetry.lock b/backend/poetry.lock index 7ecdcb9c2..5273b20bc 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1666,3 +1666,4 @@ wrapt = [ {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, ] + From 8d7f6aa3ab17fe42b1fcf2a57ec7fe048e3c7d00 Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 11:06:06 +0100 Subject: [PATCH 035/536] debug print --- .github/workflows/backend.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index c2d77d7aa..cb7730571 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -47,6 +47,7 @@ jobs: path: /usr/local/lib/python3.10/site-packages key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} + - run: pip list -v - run: python -m pytest Lint: From da48ada0e722949710f09141d4809a134068a297 Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 11:13:16 +0100 Subject: [PATCH 036/536] remove newline --- backend/poetry.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index 5273b20bc..7ecdcb9c2 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1666,4 +1666,3 @@ wrapt = [ {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, ] - From 73fc8881c367266bc5032484af2cabf6da80e4f4 Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 11:18:21 +0100 Subject: [PATCH 037/536] update packages --- .github/workflows/backend.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index cb7730571..e09df0dbb 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -36,6 +36,9 @@ jobs: - if: steps.cache.outputs.cache-hit != 'true' run: POETRY_VIRTUALENVS_CREATE=false python -m poetry install -n + - run: POETRY_VIRTUALENVS_CREATE=false python -m poetry update + - run: pip list -v + Test: needs: [Dependencies] runs-on: self-hosted From 88230c6dcf105a866a31379723db2b4bead6b7ed Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 11:21:16 +0100 Subject: [PATCH 038/536] trigger clean build --- .github/workflows/backend.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index e09df0dbb..2ed0648e9 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -20,9 +20,9 @@ jobs: id: cache with: path: /usr/local/lib/python3.10/site-packages - key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-bullseye-3.10.2-${{ hashFiles('**/poetry.lock') }} restore-keys: | - venv-${{ runner.os }}-bullseye-3.10- + venv-${{ runner.os }}-bullseye-3.10.2- - if: steps.cache.outputs.cache-hit != 'true' run: apt update @@ -48,7 +48,7 @@ jobs: - uses: actions/cache@v2 with: path: /usr/local/lib/python3.10/site-packages - key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-bullseye-3.10.2-${{ hashFiles('**/poetry.lock') }} - run: pip list -v - run: python -m pytest @@ -62,7 +62,7 @@ jobs: - uses: actions/cache@v2 with: path: /usr/local/lib/python3.10/site-packages - key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-bullseye-3.10.2-${{ hashFiles('**/poetry.lock') }} - run: python -m pylint src tests @@ -75,6 +75,6 @@ jobs: - uses: actions/cache@v2 with: path: /usr/local/lib/python3.10/site-packages - key: venv-${{ runner.os }}-bullseye-3.10-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-bullseye-3.10.2-${{ hashFiles('**/poetry.lock') }} - run: python -m mypy src tests From e12f349be378c807f427a2ccf862e84ea8de8efd Mon Sep 17 00:00:00 2001 From: Francis Date: Thu, 17 Mar 2022 11:26:54 +0100 Subject: [PATCH 039/536] always install --- .github/workflows/backend.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 2ed0648e9..a0869e604 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -33,9 +33,10 @@ jobs: - if: steps.cache.outputs.cache-hit != 'true' run: pip install -U pip poetry - - if: steps.cache.outputs.cache-hit != 'true' - run: POETRY_VIRTUALENVS_CREATE=false python -m poetry install -n + #- if: steps.cache.outputs.cache-hit != 'true' + # run: POETRY_VIRTUALENVS_CREATE=false python -m poetry install --no-interaction + - run: POETRY_VIRTUALENVS_CREATE=false python -m poetry install --no-interaction - run: POETRY_VIRTUALENVS_CREATE=false python -m poetry update - run: pip list -v From 1d1eb633546bdda724bc4436939b5b26065484a9 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Thu, 17 Mar 2022 14:54:34 +0100 Subject: [PATCH 040/536] working on the route --- backend/src/app/logic/suggestions.py | 7 +++++ .../app/routers/editions/students/students.py | 8 +++-- .../suggestions/students_suggestions.py | 26 ++++++++++++---- backend/src/app/schemas/suggestion.py | 9 ++++++ backend/src/database/crud/suggestions.py | 4 +-- .../test_crud/test_suggestions.py | 14 ++++----- .../test_editions/test_students/__init__.py | 0 .../test_suggestions/__init__.py | 0 .../test_suggestions/test_suggestions.py | 30 +++++++++++++++++++ 9 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 backend/src/app/logic/suggestions.py create mode 100644 backend/src/app/schemas/suggestion.py create mode 100644 backend/tests/test_routers/test_editions/test_students/__init__.py create mode 100644 backend/tests/test_routers/test_editions/test_students/test_suggestions/__init__.py create mode 100644 backend/tests/test_routers/test_editions/test_students/test_suggestions/test_suggestions.py diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py new file mode 100644 index 000000000..252aa4c30 --- /dev/null +++ b/backend/src/app/logic/suggestions.py @@ -0,0 +1,7 @@ +from sqlalchemy.orm import Session + +from src.app.schemas.suggestion import NewSuggestion +from database.crud.suggestions import create_suggestion + +def new_suggestion(db: Session, new_suggestion: NewSuggestion): + pass \ No newline at end of file diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index fbdd55bda..c374c973a 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -1,14 +1,18 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session from src.app.routers.tags import Tags from .suggestions import students_suggestions_router +from src.app.utils.dependencies import get_edition, require_authorization +from src.database.database import get_session +from src.database.models import Edition, User students_router = APIRouter(prefix="/students", tags=[Tags.STUDENTS]) students_router.include_router(students_suggestions_router, prefix="/{student_id}") @students_router.get("/") -async def get_students(edition_id: int): +async def get_students(db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): """ Get a list of all students. """ diff --git a/backend/src/app/routers/editions/students/suggestions/students_suggestions.py b/backend/src/app/routers/editions/students/suggestions/students_suggestions.py index aac361f44..c30516cc0 100644 --- a/backend/src/app/routers/editions/students/suggestions/students_suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/students_suggestions.py @@ -1,26 +1,42 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session from src.app.routers.tags import Tags +from src.app.utils.dependencies import get_edition, require_authorization +from src.database.database import get_session +from src.database.models import Edition, User + students_suggestions_router = APIRouter(prefix="/suggestions", tags=[Tags.STUDENTS]) @students_suggestions_router.post("/") -async def create_suggestion(edition_id: int, student_id: int): +async def create_suggestion(db: Session = Depends(get_session), edition: Edition = Depends(get_edition), user: User = Depends(require_authorization)): """ Make a suggestion about a student. """ + return user @students_suggestions_router.get("/{suggestion_id}") -async def delete_suggestion(edition_id: int, student_id: int, suggestion_id: int): +async def get_suggestion(db: Session = Depends(get_session), edition: Edition = Depends(get_edition), user: User = Depends(require_authorization)): """ - Delete a suggestion you made about a student. + Get all suggestions of a student. """ + #test_client.post("/editions/1/students/1/suggestions/") -@students_suggestions_router.put("/{suggestion_id}") +@students_suggestions_router.put("/{suggestion_id}", dependencies=[Depends(require_authorization)]) async def edit_suggestion(edition_id: int, student_id: int, suggestion_id: int): """ Edit a suggestion you made about a student. """ + +@students_suggestions_router.delete("/{suggestion_id}", dependencies=[Depends(require_authorization)]) +async def delete_suggestion(edition_id: int, suggestion_id: int): + """ + Delete a suggestion you made about a student. + """ + + +#user: NewUser, db: Session = Depends(get_session), edition: Edition = Depends(get_edition) \ No newline at end of file diff --git a/backend/src/app/schemas/suggestion.py b/backend/src/app/schemas/suggestion.py new file mode 100644 index 000000000..68ef27c2c --- /dev/null +++ b/backend/src/app/schemas/suggestion.py @@ -0,0 +1,9 @@ +from src.app.schemas.webhooks import CamelCaseModel +from src.database.enums import DecisionEnum + +class NewSuggestion(CamelCaseModel): + """The fields of a suggestion""" + student_id: int + coach_id: int + suggestion: DecisionEnum + argumentation: str \ No newline at end of file diff --git a/backend/src/database/crud/suggestions.py b/backend/src/database/crud/suggestions.py index 31704c66b..ca3f9f639 100644 --- a/backend/src/database/crud/suggestions.py +++ b/backend/src/database/crud/suggestions.py @@ -3,8 +3,8 @@ from src.database.models import Suggestion, Student, User from src.database.enums import DecisionEnum -def create_suggestion(db: Session, user: User, student: Student, decision: DecisionEnum, argumentation: str) -> None: - suggestion: Suggestion = Suggestion(student=student, coach=user,suggestion=decision,argumentation=argumentation) +def create_suggestion(db: Session, user_id: int, student_id: int, decision: DecisionEnum, argumentation: str) -> None: + suggestion: Suggestion = Suggestion(student_id=student_id, coach_id=user_id,suggestion=decision,argumentation=argumentation) db.add(suggestion) db.commit() diff --git a/backend/tests/test_database/test_crud/test_suggestions.py b/backend/tests/test_database/test_crud/test_suggestions.py index d6568d916..d823c6e39 100644 --- a/backend/tests/test_database/test_crud/test_suggestions.py +++ b/backend/tests/test_database/test_crud/test_suggestions.py @@ -14,7 +14,7 @@ def test_create_suggestion_yes(database_session: Session): user: User = database_session.query(User).where(User.name == "coach1").first() student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() - create_suggestion(database_session, user, student, DecisionEnum.YES, "This is a good student") + create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user and Suggestion.student == student).first() @@ -29,7 +29,7 @@ def test_create_suggestion_no(database_session: Session): user: User = database_session.query(User).where(User.name == "coach1").first() student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() - create_suggestion(database_session, user, student, DecisionEnum.NO, "This is a not good student") + create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.NO, "This is a not good student") suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user and Suggestion.student == student).first() @@ -44,7 +44,7 @@ def test_create_suggestion_maybe(database_session: Session): user: User = database_session.query(User).where(User.name == "coach1").first() student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() - create_suggestion(database_session, user, student, DecisionEnum.MAYBE, "Idk if it's good student") + create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user and Suggestion.student == student).first() @@ -59,9 +59,9 @@ def test_multiple_suggestions_about_same_student(database_session: Session): user: User = database_session.query(User).where(User.name == "coach1").first() student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() - create_suggestion(database_session, user, student, DecisionEnum.MAYBE, "Idk if it's good student") + create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") with pytest.raises(IntegrityError): - create_suggestion(database_session, user, student, DecisionEnum.YES, "This is a good student") + create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") def test_get_suggestions_of_student(database_session: Session): fill_database(database_session) @@ -70,8 +70,8 @@ def test_get_suggestions_of_student(database_session: Session): user2: User = database_session.query(User).where(User.name == "coach2").first() student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() - create_suggestion(database_session, user1, student, DecisionEnum.MAYBE, "Idk if it's good student") - create_suggestion(database_session, user2, student, DecisionEnum.YES, "This is a good student") + create_suggestion(database_session, user1.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") + create_suggestion(database_session, user2.user_id, student.student_id, DecisionEnum.YES, "This is a good student") suggestions_student = get_suggestions_of_student(database_session, student.student_id) assert len(suggestions_student) == 2 diff --git a/backend/tests/test_routers/test_editions/test_students/__init__.py b/backend/tests/test_routers/test_editions/test_students/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/tests/test_routers/test_editions/test_students/test_suggestions/__init__.py b/backend/tests/test_routers/test_editions/test_students/test_suggestions/__init__.py new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..e9bc385db --- /dev/null +++ b/backend/tests/test_routers/test_editions/test_students/test_suggestions/test_suggestions.py @@ -0,0 +1,30 @@ +from sqlalchemy.orm import Session +from starlette import status +from starlette.testclient import TestClient +from tests.fill_database import fill_database +from src.database.models import Suggestion, Student, User + + +#def test_test(database_session: Session, test_client: TestClient): +# fill_database(database_session) +# email = "coach1@noutlook.be" +# password = "wachtwoord" +# form = { +# "username": email, +# "password": password +# } +# d = test_client.post("/login/token", data=form).json() +# print(d) +# r = test_client.post("/editions/1/students/1/suggestions/", headers={"Authorization": str(d)}) +# #r = test_client.post("/editions/1/students/1/suggestions/") +# print(r.json()) +# assert False + + +""" +def test_ok(database_session: Session, test_client: TestClient): + database_session.add(Edition(year=2022)) + database_session.commit() + response = test_client.post("/editions/1/register/email", json={"name": "Joskes vermeulen","email": "jw@gmail.com", "pw": "test"}) + assert response.status_code == status. +""" \ No newline at end of file From 75c5869d3260c344733e4b89d72ff7faf842087d Mon Sep 17 00:00:00 2001 From: FKD13 Date: Thu, 17 Mar 2022 22:57:01 +0100 Subject: [PATCH 041/536] use pip for installation --- .github/workflows/backend.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index a0869e604..23fd66718 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -26,18 +26,17 @@ jobs: - if: steps.cache.outputs.cache-hit != 'true' run: apt update - + - if: steps.cache.outputs.cache-hit != 'true' run: apt install -y build-essential libmariadb-dev - + - if: steps.cache.outputs.cache-hit != 'true' run: pip install -U pip poetry - #- if: steps.cache.outputs.cache-hit != 'true' + # - if: steps.cache.outputs.cache-hit != 'true' # run: POETRY_VIRTUALENVS_CREATE=false python -m poetry install --no-interaction - - run: POETRY_VIRTUALENVS_CREATE=false python -m poetry install --no-interaction - - run: POETRY_VIRTUALENVS_CREATE=false python -m poetry update + - run: pip install -r <(poetry export --format requirements.txt) - run: pip list -v Test: From ad19b5d877f29e6a0341e929f2ce6ba8266301fc Mon Sep 17 00:00:00 2001 From: FKD13 Date: Thu, 17 Mar 2022 23:00:01 +0100 Subject: [PATCH 042/536] dont use bash specifics --- .github/workflows/backend.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 23fd66718..e31b2689b 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -36,7 +36,9 @@ jobs: # - if: steps.cache.outputs.cache-hit != 'true' # run: POETRY_VIRTUALENVS_CREATE=false python -m poetry install --no-interaction - - run: pip install -r <(poetry export --format requirements.txt) + - run: | + poetry export --format requirements.txt --output requirements.txt + pip install -r requirements.txt - run: pip list -v Test: From 79f324c1bd8ac1bcc91c3d00a478d00636fdf6ef Mon Sep 17 00:00:00 2001 From: FKD13 Date: Thu, 17 Mar 2022 23:03:56 +0100 Subject: [PATCH 043/536] python -m --- .github/workflows/backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index e31b2689b..e2929cb34 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -37,7 +37,7 @@ jobs: # run: POETRY_VIRTUALENVS_CREATE=false python -m poetry install --no-interaction - run: | - poetry export --format requirements.txt --output requirements.txt + python -m poetry export --format requirements.txt --output requirements.txt pip install -r requirements.txt - run: pip list -v From baca0c5abb38632a1339c302f71600f3b254d63f Mon Sep 17 00:00:00 2001 From: FKD13 Date: Thu, 17 Mar 2022 23:05:57 +0100 Subject: [PATCH 044/536] force cache miss by adding version number --- .github/workflows/backend.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index e2929cb34..41a990a3e 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -20,9 +20,9 @@ jobs: id: cache with: path: /usr/local/lib/python3.10/site-packages - key: venv-${{ runner.os }}-bullseye-3.10.2-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-bullseye-3.10.2-v1-${{ hashFiles('**/poetry.lock') }} restore-keys: | - venv-${{ runner.os }}-bullseye-3.10.2- + venv-${{ runner.os }}-bullseye-3.10.2-v1 - if: steps.cache.outputs.cache-hit != 'true' run: apt update @@ -33,10 +33,8 @@ jobs: - if: steps.cache.outputs.cache-hit != 'true' run: pip install -U pip poetry - # - if: steps.cache.outputs.cache-hit != 'true' - # run: POETRY_VIRTUALENVS_CREATE=false python -m poetry install --no-interaction - - - run: | + - if: steps.cache.outputs.cache-hit != 'true' + run: | python -m poetry export --format requirements.txt --output requirements.txt pip install -r requirements.txt - run: pip list -v @@ -50,7 +48,7 @@ jobs: - uses: actions/cache@v2 with: path: /usr/local/lib/python3.10/site-packages - key: venv-${{ runner.os }}-bullseye-3.10.2-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-bullseye-3.10.2-v1-${{ hashFiles('**/poetry.lock') }} - run: pip list -v - run: python -m pytest @@ -64,7 +62,7 @@ jobs: - uses: actions/cache@v2 with: path: /usr/local/lib/python3.10/site-packages - key: venv-${{ runner.os }}-bullseye-3.10.2-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-bullseye-3.10.2-v1-${{ hashFiles('**/poetry.lock') }} - run: python -m pylint src tests @@ -77,6 +75,6 @@ jobs: - uses: actions/cache@v2 with: path: /usr/local/lib/python3.10/site-packages - key: venv-${{ runner.os }}-bullseye-3.10.2-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-bullseye-3.10.2-v1-${{ hashFiles('**/poetry.lock') }} - run: python -m mypy src tests From 6f4469c0ada2804023e7abd40a4c1e405b19ffd9 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Thu, 17 Mar 2022 23:09:36 +0100 Subject: [PATCH 045/536] add dev requirements --- .github/workflows/backend.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 41a990a3e..180451dea 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -20,7 +20,7 @@ jobs: id: cache with: path: /usr/local/lib/python3.10/site-packages - key: venv-${{ runner.os }}-bullseye-3.10.2-v1-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-bullseye-3.10.2-v1-${{ hashFiles('**/poetry.lock', '**/backend.yml') }} restore-keys: | venv-${{ runner.os }}-bullseye-3.10.2-v1 @@ -35,7 +35,7 @@ jobs: - if: steps.cache.outputs.cache-hit != 'true' run: | - python -m poetry export --format requirements.txt --output requirements.txt + python -m poetry export --dev --format requirements.txt --output requirements.txt pip install -r requirements.txt - run: pip list -v From 1c1b7db3857ac4f49b32f15f29066247e237170b Mon Sep 17 00:00:00 2001 From: FKD13 Date: Thu, 17 Mar 2022 23:13:29 +0100 Subject: [PATCH 046/536] dont use hashes --- .github/workflows/backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 180451dea..07faa9945 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -35,7 +35,7 @@ jobs: - if: steps.cache.outputs.cache-hit != 'true' run: | - python -m poetry export --dev --format requirements.txt --output requirements.txt + python -m poetry export --dev --format requirements.txt --output requirements.txt --without-hashes pip install -r requirements.txt - run: pip list -v From a08b83ef719292129d31953e7103678c0549ba84 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Thu, 17 Mar 2022 23:16:59 +0100 Subject: [PATCH 047/536] use same version everywhere --- .github/workflows/backend.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 07faa9945..d513e3368 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -20,9 +20,9 @@ jobs: id: cache with: path: /usr/local/lib/python3.10/site-packages - key: venv-${{ runner.os }}-bullseye-3.10.2-v1-${{ hashFiles('**/poetry.lock', '**/backend.yml') }} + key: venv-${{ runner.os }}-bullseye-3.10.2-v2-${{ hashFiles('**/poetry.lock') }} restore-keys: | - venv-${{ runner.os }}-bullseye-3.10.2-v1 + venv-${{ runner.os }}-bullseye-3.10.2-v2 - if: steps.cache.outputs.cache-hit != 'true' run: apt update @@ -48,7 +48,7 @@ jobs: - uses: actions/cache@v2 with: path: /usr/local/lib/python3.10/site-packages - key: venv-${{ runner.os }}-bullseye-3.10.2-v1-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-bullseye-3.10.2-v2-${{ hashFiles('**/poetry.lock') }} - run: pip list -v - run: python -m pytest @@ -62,7 +62,7 @@ jobs: - uses: actions/cache@v2 with: path: /usr/local/lib/python3.10/site-packages - key: venv-${{ runner.os }}-bullseye-3.10.2-v1-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-bullseye-3.10.2-v2-${{ hashFiles('**/poetry.lock') }} - run: python -m pylint src tests @@ -75,6 +75,6 @@ jobs: - uses: actions/cache@v2 with: path: /usr/local/lib/python3.10/site-packages - key: venv-${{ runner.os }}-bullseye-3.10.2-v1-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-bullseye-3.10.2-v2-${{ hashFiles('**/poetry.lock') }} - run: python -m mypy src tests From 8850cd37a8d0c2da598cf08bb2252f1238e351b3 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Thu, 17 Mar 2022 23:20:06 +0100 Subject: [PATCH 048/536] remove empty requirements files --- backend/requirements-dev.txt | 0 backend/requirements.txt | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 backend/requirements-dev.txt delete mode 100644 backend/requirements.txt diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/requirements.txt b/backend/requirements.txt deleted file mode 100644 index e69de29bb..000000000 From 7e280c407e11bec3b834ef6fb969fe4518159d6a Mon Sep 17 00:00:00 2001 From: FKD13 Date: Thu, 17 Mar 2022 23:22:16 +0100 Subject: [PATCH 049/536] remove debug lines --- .github/workflows/backend.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index d513e3368..dbe878a58 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -37,7 +37,6 @@ jobs: run: | python -m poetry export --dev --format requirements.txt --output requirements.txt --without-hashes pip install -r requirements.txt - - run: pip list -v Test: needs: [Dependencies] @@ -50,7 +49,6 @@ jobs: path: /usr/local/lib/python3.10/site-packages key: venv-${{ runner.os }}-bullseye-3.10.2-v2-${{ hashFiles('**/poetry.lock') }} - - run: pip list -v - run: python -m pytest Lint: From 6bca8ed0f0e47bdba946652a67dbab5d550d704d Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 18 Mar 2022 10:12:54 +0100 Subject: [PATCH 050/536] working on the suggestions + tests --- backend/src/app/logic/suggestions.py | 11 ++-- .../editions/students/suggestions/__init__.py | 2 +- ...students_suggestions.py => suggestions.py} | 17 +++--- backend/src/app/schemas/suggestion.py | 2 - .../test_suggestions/test_suggestions.py | 53 ++++++++++++++----- 5 files changed, 58 insertions(+), 27 deletions(-) rename backend/src/app/routers/editions/students/suggestions/{students_suggestions.py => suggestions.py} (56%) diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py index 252aa4c30..d8a7d372e 100644 --- a/backend/src/app/logic/suggestions.py +++ b/backend/src/app/logic/suggestions.py @@ -1,7 +1,12 @@ from sqlalchemy.orm import Session from src.app.schemas.suggestion import NewSuggestion -from database.crud.suggestions import create_suggestion +from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student +from src.database.models import Suggestion, User -def new_suggestion(db: Session, new_suggestion: NewSuggestion): - pass \ No newline at end of file +def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, user: User, student_id: int): + create_suggestion(db, user.user_id, student_id, new_suggestion.suggestion, new_suggestion.argumentation) + +def all_suggestions_of_student(db: Session, student_id: int) -> list[Suggestion]: + return get_suggestions_of_student(db, student_id) +#def get_suggestions(db: ) \ No newline at end of file diff --git a/backend/src/app/routers/editions/students/suggestions/__init__.py b/backend/src/app/routers/editions/students/suggestions/__init__.py index b2a4c460b..34c20941b 100644 --- a/backend/src/app/routers/editions/students/suggestions/__init__.py +++ b/backend/src/app/routers/editions/students/suggestions/__init__.py @@ -1 +1 @@ -from .students_suggestions import students_suggestions_router +from .suggestions import students_suggestions_router diff --git a/backend/src/app/routers/editions/students/suggestions/students_suggestions.py b/backend/src/app/routers/editions/students/suggestions/suggestions.py similarity index 56% rename from backend/src/app/routers/editions/students/suggestions/students_suggestions.py rename to backend/src/app/routers/editions/students/suggestions/suggestions.py index c30516cc0..d8912b343 100644 --- a/backend/src/app/routers/editions/students/suggestions/students_suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/suggestions.py @@ -1,29 +1,32 @@ from fastapi import APIRouter, Depends from sqlalchemy.orm import Session +from starlette import status from src.app.routers.tags import Tags from src.app.utils.dependencies import get_edition, require_authorization from src.database.database import get_session -from src.database.models import Edition, User +from src.database.models import Edition, Student, User +from src.app.logic.suggestions import make_new_suggestion, get_suggestions_of_student +from src.app.schemas.suggestion import NewSuggestion students_suggestions_router = APIRouter(prefix="/suggestions", tags=[Tags.STUDENTS]) -@students_suggestions_router.post("/") -async def create_suggestion(db: Session = Depends(get_session), edition: Edition = Depends(get_edition), user: User = Depends(require_authorization)): +@students_suggestions_router.post("/", status_code=status.HTTP_201_CREATED) +async def create_suggestion(student_id: int, new_suggestion: NewSuggestion, db: Session = Depends(get_session), user: User = Depends(require_authorization)): """ Make a suggestion about a student. """ - return user + make_new_suggestion(db, new_suggestion, user, student_id) -@students_suggestions_router.get("/{suggestion_id}") -async def get_suggestion(db: Session = Depends(get_session), edition: Edition = Depends(get_edition), user: User = Depends(require_authorization)): +@students_suggestions_router.get("/{suggestion_id}", dependencies=[Depends(require_authorization)], status_code=status.HTTP_200_OK) +async def get_suggestion(student_id: int, db: Session = Depends(get_session)): """ Get all suggestions of a student. """ - #test_client.post("/editions/1/students/1/suggestions/") + return get_suggestions_of_student(db, student_id) @students_suggestions_router.put("/{suggestion_id}", dependencies=[Depends(require_authorization)]) diff --git a/backend/src/app/schemas/suggestion.py b/backend/src/app/schemas/suggestion.py index 68ef27c2c..bead93d0f 100644 --- a/backend/src/app/schemas/suggestion.py +++ b/backend/src/app/schemas/suggestion.py @@ -3,7 +3,5 @@ class NewSuggestion(CamelCaseModel): """The fields of a suggestion""" - student_id: int - coach_id: int suggestion: DecisionEnum argumentation: str \ No newline at end of file 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 e9bc385db..e9b5cb7aa 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 @@ -3,22 +3,47 @@ from starlette.testclient import TestClient from tests.fill_database import fill_database from src.database.models import Suggestion, Student, User +from src.app.utils.dependencies import get_current_active_user -#def test_test(database_session: Session, test_client: TestClient): -# fill_database(database_session) -# email = "coach1@noutlook.be" -# password = "wachtwoord" -# form = { -# "username": email, -# "password": password -# } -# d = test_client.post("/login/token", data=form).json() -# print(d) -# r = test_client.post("/editions/1/students/1/suggestions/", headers={"Authorization": str(d)}) -# #r = test_client.post("/editions/1/students/1/suggestions/") -# print(r.json()) -# assert False +def test_new_suggestion(database_session: Session, test_client: TestClient): + fill_database(database_session) + email = "coach1@noutlook.be" + password = "wachtwoord" + form = { + "username": email, + "password": password + } + d = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer "+d + assert test_client.post("/editions/1/students/29/suggestions/", headers={"Authorization": auth}, json={"suggestion":1, "argumentation":"test"}).status_code == status.HTTP_201_CREATED + suggestions: list[Suggestion] = database_session.query(Suggestion).where(Suggestion.student_id==29).all() + assert len(suggestions) > 0 + +def test_new_suggestion_not_authorized(database_session: Session, test_client: TestClient): + assert test_client.post("/editions/1/students/29/suggestions/", json={"suggestion":1, "argumentation":"test"}).status_code == status.HTTP_401_UNAUTHORIZED + suggestions: list[Suggestion] = database_session.query(Suggestion).where(Suggestion.student_id==29).all() + assert len(suggestions) == 0 + +def test_get_suggestions_of_student(database_session: Session, test_client: TestClient): + fill_database(database_session) + form = { + "username": "coach1@noutlook.be", + "password": "wachtwoord" + } + d = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer "+d + assert test_client.post("/editions/1/students/29/suggestions/", headers={"Authorization": auth}, json={"suggestion":1, "argumentation":"test"}).status_code == status.HTTP_201_CREATED + form = { + "username": "admin@ngmail.com", + "password": "wachtwoord" + } + d = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer "+d + assert test_client.post("/editions/1/students/29/suggestions/", headers={"Authorization": auth}, json={"suggestion":1, "argumentation":"test"}).status_code == status.HTTP_201_CREATED + + +#, json={"suggestion":"OK", "argumentation":"test"} """ From 0b1c9558fb25b2d5d1b01541c44908ecb60a1874 Mon Sep 17 00:00:00 2001 From: beguille Date: Fri, 11 Mar 2022 17:35:30 +0100 Subject: [PATCH 051/536] #91,#92,#93,#94,#95 started work on projects routes --- backend/src/app/logic/projects.py | 39 ++++++++++++++++ .../app/routers/editions/projects/projects.py | 45 +++++++++++++------ backend/src/app/schemas/projects.py | 40 +++++++++++++++++ backend/src/database/crud/projects.py | 42 +++++++++++++++++ 4 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 backend/src/app/logic/projects.py create mode 100644 backend/src/app/schemas/projects.py create mode 100644 backend/src/database/crud/projects.py diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py new file mode 100644 index 000000000..048aef367 --- /dev/null +++ b/backend/src/app/logic/projects.py @@ -0,0 +1,39 @@ +from sqlalchemy.orm import Session + +from src.app.schemas.projects import ProjectList, Project, ConflictProjectList, ConflictProject +from src.database.crud.projects import db_get_all_projects, db_add_project, db_get_project, db_delete_project, \ + db_patch_project, db_get_conflict_students_for_project, db_get_all_conflict_projects +from src.database.models import Edition + + +def get_project_list(db: Session, edition: Edition) -> ProjectList: + db_all_projects = db_get_all_projects(db, edition) + return ProjectList(projects=db_all_projects) + + +def logic_create_project(db: Session, edition: Edition, name: str, number_of_students: int): + db_add_project(db, edition, name, number_of_students) + + +def logic_get_project(db: Session, project_id: int) -> Project: + project = db_get_project(db, project_id) + return project + + +def logic_delete_project(db: Session, project_id: int): + db_delete_project(db, project_id) + + +def logic_patch_project(db: Session, project_id: int, name: str, number_of_students: int): + db_patch_project(db, project_id, name, number_of_students) + + +def logic_get_conflicts(db: Session, edition: Edition) -> ConflictProjectList: + output = [] + projects = db_get_all_conflict_projects(db, edition) + for project in projects: + students = db_get_conflict_students_for_project(db, project) + output.append(ConflictProject(project_id=project.project_id, name=project.name, + number_of_students=project.number_of_students, + edition_id=project.edition_id, conflicting_students=students)) + return ConflictProjectList(output) diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index 5b873757a..9bcfdc24f 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -1,50 +1,67 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from starlette import status +from starlette.responses import Response +from src.app.logic.projects import get_project_list, logic_create_project, logic_get_project, logic_delete_project, \ + logic_patch_project, logic_get_conflicts from src.app.routers.tags import Tags +from src.app.schemas.projects import ProjectList, Project, ConflictProjectList +from src.app.utils.dependencies import get_edition +from src.database.database import get_session +from src.database.models import Edition 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.get("/") -async def get_projects(edition_id: int): +@projects_router.get("/", response_model=ProjectList) +async def get_projects(db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): """ Get a list of all projects. """ + return get_project_list(db, edition) -@projects_router.post("/") -async def create_project(edition_id: int): +@projects_router.post("/", status_code=status.HTTP_201_CREATED, response_class=Response) +async def create_project(name: str, number_of_students: int, db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): """ - Create a new project. + Create a new project.users """ + logic_create_project(db, edition, name, number_of_students) -@projects_router.get("/conflicts") -async def get_conflicts(edition_id: int): +@projects_router.get("/conflicts", response_model=ConflictProjectList) +async def get_conflicts(db: Session = 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 all projects which have more students than listed, and all students who are in more than one project + return logic_get_conflicts(db, edition) -@projects_router.delete("/{project_id}") -async def delete_project(edition_id: int, project_id: int): +@projects_router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response) +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.get("/{project_id}") -async def get_project(edition_id: int, project_id: int): +@projects_router.get("/{project_id}", status_code=status.HTTP_200_OK, response_model=Project) +async def get_project(project_id: int, db: Session = Depends(get_session)): """ Get information about a specific project. """ + return logic_get_project(db, project_id) -@projects_router.patch("/{project_id}") -async def patch_project(edition_id: int, project_id: int): +@projects_router.patch("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response) +async def patch_project(project_id: int, name: str, number_of_students: int, db: Session = Depends(get_session)): """ Update a project, changing some fields. """ + logic_patch_project(db, project_id, name, number_of_students) + diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py new file mode 100644 index 000000000..e914589bc --- /dev/null +++ b/backend/src/app/schemas/projects.py @@ -0,0 +1,40 @@ +from src.app.schemas.webhooks import CamelCaseModel +from src.database.enums import DecisionEnum + + +class Project(CamelCaseModel): + project_id: int + name: str + number_of_students: int + edition_id: int + + class Config: + orm_mode = True + + +class Student(CamelCaseModel): + student_id: int + first_name: str + last_name: str + preferred_name: str + email_address: str + phone_number: str + alumni: bool + decision: DecisionEnum + wants_to_be_student_coach: bool + edition_id: int + + class Config: + orm_mode = True + + +class ProjectList(CamelCaseModel): + projects: list[Project] + + +class ConflictProject(Project): + conflicting_students = list[Student] + + +class ConflictProjectList(CamelCaseModel): + conflict_projects = list[ConflictProject] diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py new file mode 100644 index 000000000..f2080b0d2 --- /dev/null +++ b/backend/src/database/crud/projects.py @@ -0,0 +1,42 @@ +from sqlalchemy.orm import Session + +from src.database.models import Project, Edition, Student, ProjectRole + + +def db_get_all_projects(db: Session, edition: Edition) -> list[Project]: + return db.query(Project).where(Project.edition == edition).all() + + +def db_add_project(db: Session, edition: Edition, name: str, number_of_students: int): + project = Project(name=name, number_of_students=number_of_students, edition_id=edition.edition_id) + db.add(project) + db.commit() + + +def db_get_project(db: Session, project_id: int) -> Project: + return db.query(Project).where(Project.project_id == project_id).one() + + +def db_delete_project(db: Session, project_id: int): + project = db_get_project(db, project_id) + db.delete(project) + db.commit() + + +def db_patch_project(db: Session, project_id: int, name: str, number_of_students: int): + project = db_get_project(db, project_id) + project.name = name + project.number_of_students = number_of_students + db.commit() + + +def db_get_all_conflict_projects(db: Session, edition: Edition) -> list[Project]: + return db.query(Project).where(len(Project.project_roles) > Project.number_of_students)\ + .where(Project.edition == edition).all() + + +def db_get_conflict_students_for_project(db: Session, project: Project) -> list[Student]: + students = db.query(ProjectRole.student).where(ProjectRole.project_id == project.project_id)\ + .where(len(ProjectRole.student.project_roles) > 1).all() + + return students From 804eab46db88813dbd36352fdbef39538017f854 Mon Sep 17 00:00:00 2001 From: beguille Date: Sun, 13 Mar 2022 15:49:05 +0100 Subject: [PATCH 052/536] nearly done with projects_students --- backend/src/app/logic/projects.py | 21 ++++----- backend/src/app/logic/projects_students.py | 17 +++++++ .../app/routers/editions/projects/projects.py | 22 +++++---- .../projects/students/projects_students.py | 28 ++++++++--- backend/src/app/schemas/projects.py | 43 +++++++++++++++++ backend/src/app/utils/dependencies.py | 8 +++- backend/src/database/crud/projects.py | 47 ++++++++++++++++--- .../src/database/crud/projects_students.py | 22 +++++++++ 8 files changed, 171 insertions(+), 37 deletions(-) create mode 100644 backend/src/app/logic/projects_students.py create mode 100644 backend/src/database/crud/projects_students.py diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index 048aef367..7964ece28 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -1,31 +1,28 @@ from sqlalchemy.orm import Session from src.app.schemas.projects import ProjectList, Project, ConflictProjectList, ConflictProject -from src.database.crud.projects import db_get_all_projects, db_add_project, db_get_project, db_delete_project, \ +from src.database.crud.projects import db_get_all_projects, db_add_project, db_delete_project, \ db_patch_project, db_get_conflict_students_for_project, db_get_all_conflict_projects from src.database.models import Edition -def get_project_list(db: Session, edition: Edition) -> ProjectList: +def logic_get_project_list(db: Session, edition: Edition) -> ProjectList: db_all_projects = db_get_all_projects(db, edition) return ProjectList(projects=db_all_projects) -def logic_create_project(db: Session, edition: Edition, name: str, number_of_students: int): - db_add_project(db, edition, name, number_of_students) - - -def logic_get_project(db: Session, project_id: int) -> Project: - project = db_get_project(db, project_id) - return project +def logic_create_project(db: Session, edition: Edition, name: str, number_of_students: int, skills: [int], + partners: [str], coaches: [int]): + db_add_project(db, edition, name, number_of_students, skills, partners, coaches) def logic_delete_project(db: Session, project_id: int): db_delete_project(db, project_id) -def logic_patch_project(db: Session, project_id: int, name: str, number_of_students: int): - db_patch_project(db, project_id, name, number_of_students) +def logic_patch_project(db: Session, project: Project, name: str, number_of_students: int, skills: [int], + partners: [str], coaches: [int]): + db_patch_project(db, project, name, number_of_students, skills, partners, coaches) def logic_get_conflicts(db: Session, edition: Edition) -> ConflictProjectList: @@ -36,4 +33,4 @@ def logic_get_conflicts(db: Session, edition: Edition) -> ConflictProjectList: output.append(ConflictProject(project_id=project.project_id, name=project.name, number_of_students=project.number_of_students, edition_id=project.edition_id, conflicting_students=students)) - return ConflictProjectList(output) + return ConflictProjectList(conflict_projects=output) diff --git a/backend/src/app/logic/projects_students.py b/backend/src/app/logic/projects_students.py new file mode 100644 index 000000000..8fa6ea72c --- /dev/null +++ b/backend/src/app/logic/projects_students.py @@ -0,0 +1,17 @@ +from sqlalchemy.orm import Session + +from src.database.crud.projects_students import db_remove_student_project, db_add_student_project, \ + db_change_project_role +from src.database.models import Project + + +def logic_remove_student_project(db: Session, project: Project, student_id: int): + db_remove_student_project(db, project, student_id) + + +def logic_add_student_project(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): + db_add_student_project(db, project, student_id, skill_id, drafter_id) + + +def logic_change_project_role(db: Session, project: Project, student_id: int, skill_id: int): + db_change_project_role(db, project, student_id, skill_id) diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index 9bcfdc24f..516bf19ef 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -3,11 +3,11 @@ from starlette import status from starlette.responses import Response -from src.app.logic.projects import get_project_list, logic_create_project, logic_get_project, logic_delete_project, \ +from src.app.logic.projects import logic_get_project_list, logic_create_project, logic_delete_project, \ logic_patch_project, logic_get_conflicts from src.app.routers.tags import Tags from src.app.schemas.projects import ProjectList, Project, ConflictProjectList -from src.app.utils.dependencies import get_edition +from src.app.utils.dependencies import get_edition, get_project from src.database.database import get_session from src.database.models import Edition from .students import project_students_router @@ -21,15 +21,16 @@ async def get_projects(db: Session = Depends(get_session), edition: Edition = De """ Get a list of all projects. """ - return get_project_list(db, edition) + return logic_get_project_list(db, edition) @projects_router.post("/", status_code=status.HTTP_201_CREATED, response_class=Response) -async def create_project(name: str, number_of_students: int, db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): +async def create_project(name: str, number_of_students: int, skills: list[int], partners: list[str], coaches: list[int], + db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): """ Create a new project.users """ - logic_create_project(db, edition, name, number_of_students) + logic_create_project(db, edition, name, number_of_students, skills, partners, coaches) @projects_router.get("/conflicts", response_model=ConflictProjectList) @@ -51,17 +52,18 @@ async def delete_project(project_id: int, db: Session = Depends(get_session)): @projects_router.get("/{project_id}", status_code=status.HTTP_200_OK, response_model=Project) -async def get_project(project_id: int, db: Session = Depends(get_session)): +async def get_project(project: Project = Depends(get_project)): """ Get information about a specific project. """ - return logic_get_project(db, project_id) + return project @projects_router.patch("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response) -async def patch_project(project_id: int, name: str, number_of_students: int, db: Session = Depends(get_session)): +async def patch_project(name: str, number_of_students: int, skills: list[int], partners: list[str], + coaches: list[int], project: Project = Depends(get_project), + db: Session = Depends(get_session)): """ Update a project, changing some fields. """ - logic_patch_project(db, project_id, name, number_of_students) - + logic_patch_project(db, project, name, number_of_students, skills, partners, coaches) 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 ff455ced5..1a713fa31 100644 --- a/backend/src/app/routers/editions/projects/students/projects_students.py +++ b/backend/src/app/routers/editions/projects/students/projects_students.py @@ -1,28 +1,42 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from starlette import status +from starlette.responses import Response from src.app.routers.tags import Tags +from src.app.utils.dependencies import get_project +from src.database.database import get_session +from src.database.models import Project +from src.app.logic.projects_students import logic_remove_student_project, logic_add_student_project, \ + logic_change_project_role project_students_router = APIRouter(prefix="/students", tags=[Tags.PROJECTS, Tags.STUDENTS]) -@project_students_router.delete("/{student_id}") -async def remove_student_from_project(edition_id: int, project_id: int, student_id: int): +@project_students_router.delete("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response) +async def remove_student_from_project(student_id: int, db: Session = Depends(get_session), + project: Project = Depends(get_project)): """ Remove a student from a project. """ + logic_remove_student_project(db, project, student_id) -@project_students_router.patch("/{student_id}") -async def change_project_role(edition_id: int, project_id: int, student_id: int): +@project_students_router.patch("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response) +async def change_project_role(student_id: int, skill_id: int, db: Session = Depends(get_session), + project: Project = Depends(get_project)): """ Change the role a student is drafted for in a project. """ + logic_change_project_role(db, project, student_id, skill_id) -@project_students_router.post("/{student_id}") -async def add_student_to_project(edition_id: int, project_id: int, student_id: int): +@project_students_router.post("/{student_id}", status_code=status.HTTP_201_CREATED, response_class=Response) +async def add_student_to_project(student_id: int, skill_id: int, drafter_id: int, db: Session = Depends(get_session), + project: Project = Depends(get_project)): """ 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, skill_id, drafter_id) diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index e914589bc..adb36e069 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -2,12 +2,55 @@ from src.database.enums import DecisionEnum +class User(CamelCaseModel): + user_id: int + name: str + email: str + + class Config: + orm_mode = True + + +class Skill(CamelCaseModel): + skill_id: int + name: str + description: str + + class Config: + orm_mode = True + + +class Partner(CamelCaseModel): + partner_id: int + name: str + + class Config: + orm_mode = True + + +class ProjectRole(CamelCaseModel): + student_id: int + project_id: int + skill_id: int + definitive: bool + argumentation: str + drafter_id: int + + class Config: + orm_mode = True + + class Project(CamelCaseModel): project_id: int name: str number_of_students: int edition_id: int + coaches: list[User] + skills: list[Skill] + partners: list[Partner] + project_roles: list[ProjectRole] + class Config: orm_mode = True diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index 84f1929c8..00a8052d5 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -8,9 +8,10 @@ from src.app.exceptions.authentication import ExpiredCredentialsException, InvalidCredentialsException, MissingPermissionsException from src.app.logic.security import ALGORITHM, get_user_by_id from src.database.crud.editions import get_edition_by_id +from src.database.crud.projects import db_get_project from src.database.crud.invites import get_invite_link_by_uuid from src.database.database import get_session -from src.database.models import Edition, InviteLink, User +from src.database.models import Edition, InviteLink, User, Project # TODO: Might be nice to use a more descriptive year number here than primary id. @@ -65,3 +66,8 @@ async def require_admin(user: User = Depends(get_current_active_user)) -> User: def get_invite_link(invite_uuid: str, db: Session = 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) + + +def get_project(project_id: int, db: Session = Depends(get_session)) -> Project: + """Get a project from het database, given the id in the path""" + return db_get_project(db, project_id) diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index f2080b0d2..6d9603505 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -1,14 +1,29 @@ +from sqlalchemy.exc import NoResultFound from sqlalchemy.orm import Session -from src.database.models import Project, Edition, Student, ProjectRole +from src.database.models import Project, Edition, Student, ProjectRole, Skill, User, Partner def db_get_all_projects(db: Session, edition: Edition) -> list[Project]: return db.query(Project).where(Project.edition == edition).all() -def db_add_project(db: Session, edition: Edition, name: str, number_of_students: int): - project = Project(name=name, number_of_students=number_of_students, edition_id=edition.edition_id) +def db_add_project(db: Session, edition: Edition, name: str, number_of_students: int, skills: [int], + partners: [str], coaches: [int]): + skills_obj = [db.query(Skill).where(Skill.skill_id == skill).one() for skill in skills] + coaches_obj = [db.query(User).where(User.user_id) == coach for coach in coaches] + partners_obj = [] + for partner in 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=name, number_of_students=number_of_students, edition_id=edition.edition_id, + skills=skills_obj, coaches=coaches_obj, partners=partners_obj) + db.add(project) db.commit() @@ -18,25 +33,43 @@ def db_get_project(db: Session, project_id: int) -> Project: def db_delete_project(db: Session, project_id: int): + proj_roles = db.query(ProjectRole).where(ProjectRole.project_id == project_id).all() + for pr in proj_roles: + db.delete(pr) + project = db_get_project(db, project_id) db.delete(project) db.commit() -def db_patch_project(db: Session, project_id: int, name: str, number_of_students: int): - project = db_get_project(db, project_id) +def db_patch_project(db: Session, project: Project, name: str, number_of_students: int, skills: [int], + partners: [str], coaches: [int]): + skills_obj = [db.query(Skill).where(Skill.skill_id == skill).one() for skill in skills] + coaches_obj = [db.query(User).where(User.user_id) == coach for coach in coaches] + partners_obj = [] + for partner in 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.name = name project.number_of_students = number_of_students + project.skills = skills_obj + project.coaches = coaches_obj + project.partners = partners_obj db.commit() def db_get_all_conflict_projects(db: Session, edition: Edition) -> list[Project]: - return db.query(Project).where(len(Project.project_roles) > Project.number_of_students)\ + return db.query(Project).where(len(Project.project_roles) > Project.number_of_students) \ .where(Project.edition == edition).all() def db_get_conflict_students_for_project(db: Session, project: Project) -> list[Student]: - students = db.query(ProjectRole.student).where(ProjectRole.project_id == project.project_id)\ + students = db.query(ProjectRole.student).where(ProjectRole.project_id == project.project_id) \ .where(len(ProjectRole.student.project_roles) > 1).all() return students diff --git a/backend/src/database/crud/projects_students.py b/backend/src/database/crud/projects_students.py new file mode 100644 index 000000000..23da3856f --- /dev/null +++ b/backend/src/database/crud/projects_students.py @@ -0,0 +1,22 @@ +from sqlalchemy.orm import Session + +from src.database.models import Edition, Project, ProjectRole + + +def db_remove_student_project(db: Session, project: Project, student_id: int): + pr = db.query(ProjectRole).where(ProjectRole.student_id == student_id and ProjectRole.project == project).one() + db.delete(pr) + db.commit() + + +def db_add_student_project(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): + 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() + + +def db_change_project_role(db: Session, project: Project, student_id: int, skill_id: int): + pr = db.query(ProjectRole).where(ProjectRole.student_id == student_id and ProjectRole.project == project).one() + pr.skill_id = skill_id + db.commit() From 0eee03e781190725838d1bf1cc72265c9c294de2 Mon Sep 17 00:00:00 2001 From: beguille Date: Tue, 15 Mar 2022 11:50:40 +0100 Subject: [PATCH 053/536] started work on tests, fixed some bugs --- backend/src/app/logic/projects.py | 16 +- backend/src/app/logic/projects_students.py | 4 +- .../app/routers/editions/projects/projects.py | 19 ++- .../projects/students/projects_students.py | 9 +- backend/src/app/schemas/projects.py | 31 +++- backend/src/database/crud/projects.py | 22 +-- .../src/database/crud/projects_students.py | 5 +- .../test_editions/test_projects/__init__.py | 0 .../test_projects/test_projects.py | 161 ++++++++++++++++++ 9 files changed, 225 insertions(+), 42 deletions(-) create mode 100644 backend/tests/test_routers/test_editions/test_projects/__init__.py create mode 100644 backend/tests/test_routers/test_editions/test_projects/test_projects.py diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index 7964ece28..472935404 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -1,8 +1,8 @@ from sqlalchemy.orm import Session -from src.app.schemas.projects import ProjectList, Project, ConflictProjectList, ConflictProject +from src.app.schemas.projects import ProjectList, Project, ConflictProjectList, ConflictProject, StudentList from src.database.crud.projects import db_get_all_projects, db_add_project, db_delete_project, \ - db_patch_project, db_get_conflict_students_for_project, db_get_all_conflict_projects + db_patch_project, db_get_conflict_students from src.database.models import Edition @@ -25,12 +25,6 @@ def logic_patch_project(db: Session, project: Project, name: str, number_of_stud db_patch_project(db, project, name, number_of_students, skills, partners, coaches) -def logic_get_conflicts(db: Session, edition: Edition) -> ConflictProjectList: - output = [] - projects = db_get_all_conflict_projects(db, edition) - for project in projects: - students = db_get_conflict_students_for_project(db, project) - output.append(ConflictProject(project_id=project.project_id, name=project.name, - number_of_students=project.number_of_students, - edition_id=project.edition_id, conflicting_students=students)) - return ConflictProjectList(conflict_projects=output) +def logic_get_conflicts(db: Session, edition: Edition) -> StudentList: + conflicts = db_get_conflict_students(db,edition) + return StudentList(students=conflicts) diff --git a/backend/src/app/logic/projects_students.py b/backend/src/app/logic/projects_students.py index 8fa6ea72c..3a9a58b3a 100644 --- a/backend/src/app/logic/projects_students.py +++ b/backend/src/app/logic/projects_students.py @@ -13,5 +13,5 @@ def logic_add_student_project(db: Session, project: Project, student_id: int, sk db_add_student_project(db, project, student_id, skill_id, drafter_id) -def logic_change_project_role(db: Session, project: Project, student_id: int, skill_id: int): - db_change_project_role(db, project, student_id, skill_id) +def logic_change_project_role(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): + db_change_project_role(db, project, student_id, skill_id, drafter_id) diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index 516bf19ef..7a9a7d099 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -6,7 +6,7 @@ from src.app.logic.projects import logic_get_project_list, logic_create_project, logic_delete_project, \ logic_patch_project, logic_get_conflicts from src.app.routers.tags import Tags -from src.app.schemas.projects import ProjectList, Project, ConflictProjectList +from src.app.schemas.projects import ProjectList, Project, StudentList, InputProject from src.app.utils.dependencies import get_edition, get_project from src.database.database import get_session from src.database.models import Edition @@ -25,21 +25,23 @@ async def get_projects(db: Session = Depends(get_session), edition: Edition = De @projects_router.post("/", status_code=status.HTTP_201_CREATED, response_class=Response) -async def create_project(name: str, number_of_students: int, skills: list[int], partners: list[str], coaches: list[int], +async def create_project(input_project: InputProject, db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): """ Create a new project.users """ - logic_create_project(db, edition, name, number_of_students, skills, partners, coaches) + logic_create_project(db, edition, + input_project.name, + input_project.number_of_students, + input_project.skills, input_project.partners, input_project.coaches) -@projects_router.get("/conflicts", response_model=ConflictProjectList) +@projects_router.get("/conflicts", response_model=StudentList) async def get_conflicts(db: Session = 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 all projects which have more students than listed, and all students who are in more than one project return logic_get_conflicts(db, edition) @@ -60,10 +62,11 @@ async def get_project(project: Project = Depends(get_project)): @projects_router.patch("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response) -async def patch_project(name: str, number_of_students: int, skills: list[int], partners: list[str], - coaches: list[int], project: Project = Depends(get_project), +async def patch_project(input_project: InputProject, project: Project = Depends(get_project), db: Session = Depends(get_session)): """ Update a project, changing some fields. """ - logic_patch_project(db, project, name, number_of_students, skills, partners, coaches) + logic_patch_project(db, project, input_project.name, + input_project.number_of_students, + input_project.skills, input_project.partners, input_project.coaches) 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 1a713fa31..6ba005853 100644 --- a/backend/src/app/routers/editions/projects/students/projects_students.py +++ b/backend/src/app/routers/editions/projects/students/projects_students.py @@ -4,6 +4,7 @@ from starlette.responses import Response from src.app.routers.tags import Tags +from src.app.schemas.projects import InputStudentRole from src.app.utils.dependencies import get_project from src.database.database import get_session from src.database.models import Project @@ -23,20 +24,20 @@ async def remove_student_from_project(student_id: int, db: Session = Depends(get @project_students_router.patch("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response) -async def change_project_role(student_id: int, skill_id: int, db: Session = Depends(get_session), +async def change_project_role(student_id: int, input_sr: InputStudentRole, db: Session = Depends(get_session), project: Project = Depends(get_project)): """ Change the role a student is drafted for in a project. """ - logic_change_project_role(db, project, student_id, skill_id) + logic_change_project_role(db, project, student_id, input_sr.skill_id, input_sr.drafter_id) @project_students_router.post("/{student_id}", status_code=status.HTTP_201_CREATED, response_class=Response) -async def add_student_to_project(student_id: int, skill_id: int, drafter_id: int, db: Session = Depends(get_session), +async def add_student_to_project(student_id: int, input_sr: InputStudentRole, db: Session = Depends(get_session), project: Project = Depends(get_project)): """ 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, skill_id, drafter_id) + logic_add_student_project(db, project, student_id, input_sr.skill_id, input_sr.drafter_id) diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index adb36e069..28e3c1227 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -1,3 +1,5 @@ +from pydantic import BaseModel + from src.app.schemas.webhooks import CamelCaseModel from src.database.enums import DecisionEnum @@ -9,6 +11,7 @@ class User(CamelCaseModel): class Config: orm_mode = True + allow_population_by_field_name = True class Skill(CamelCaseModel): @@ -18,6 +21,7 @@ class Skill(CamelCaseModel): class Config: orm_mode = True + allow_population_by_field_name = True class Partner(CamelCaseModel): @@ -26,6 +30,7 @@ class Partner(CamelCaseModel): class Config: orm_mode = True + allow_population_by_field_name = True class ProjectRole(CamelCaseModel): @@ -33,11 +38,12 @@ class ProjectRole(CamelCaseModel): project_id: int skill_id: int definitive: bool - argumentation: str + argumentation: str | None drafter_id: int class Config: orm_mode = True + allow_population_by_field_name = True class Project(CamelCaseModel): @@ -53,6 +59,7 @@ class Project(CamelCaseModel): class Config: orm_mode = True + allow_population_by_field_name = True class Student(CamelCaseModel): @@ -61,7 +68,7 @@ class Student(CamelCaseModel): last_name: str preferred_name: str email_address: str - phone_number: str + phone_number: str | None alumni: bool decision: DecisionEnum wants_to_be_student_coach: bool @@ -69,6 +76,11 @@ class Student(CamelCaseModel): class Config: orm_mode = True + allow_population_by_field_name = True + + +class StudentList(CamelCaseModel): + students: list[Student] class ProjectList(CamelCaseModel): @@ -81,3 +93,18 @@ class ConflictProject(Project): class ConflictProjectList(CamelCaseModel): conflict_projects = list[ConflictProject] + + +class InputProject(BaseModel): + name: str + number_of_students: int + skills: list[int] + partners: list[str] + coaches: list[int] + + +# TODO: change drafter_id to current user with authentication +class InputStudentRole(BaseModel): + skill_id: int + drafter_id: int + diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index 6d9603505..61374c03c 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -15,12 +15,11 @@ def db_add_project(db: Session, edition: Edition, name: str, number_of_students: partners_obj = [] for partner in partners: try: - partners_obj.append(db.query(Partner).where(partner.name == partner).one()) + 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=name, number_of_students=number_of_students, edition_id=edition.edition_id, skills=skills_obj, coaches=coaches_obj, partners=partners_obj) @@ -49,7 +48,7 @@ def db_patch_project(db: Session, project: Project, name: str, number_of_student partners_obj = [] for partner in partners: try: - partners_obj.append(db.query(Partner).where(partner.name == partner).one()) + partners_obj.append(db.query(Partner).where(Partner.name == partner).one()) except NoResultFound: partner_obj = Partner(name=partner) db.add(partner_obj) @@ -63,13 +62,10 @@ def db_patch_project(db: Session, project: Project, name: str, number_of_student db.commit() -def db_get_all_conflict_projects(db: Session, edition: Edition) -> list[Project]: - return db.query(Project).where(len(Project.project_roles) > Project.number_of_students) \ - .where(Project.edition == edition).all() - - -def db_get_conflict_students_for_project(db: Session, project: Project) -> list[Student]: - students = db.query(ProjectRole.student).where(ProjectRole.project_id == project.project_id) \ - .where(len(ProjectRole.student.project_roles) > 1).all() - - return students +def db_get_conflict_students(db: Session, edition: Edition) -> list[Student]: + students = db.query(Student).where(Student.edition == edition).all() + conflicts = [] + for s in students: + if len(s.project_roles) > 1: + conflicts.append(s) + return conflicts diff --git a/backend/src/database/crud/projects_students.py b/backend/src/database/crud/projects_students.py index 23da3856f..22403ae5b 100644 --- a/backend/src/database/crud/projects_students.py +++ b/backend/src/database/crud/projects_students.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session -from src.database.models import Edition, Project, ProjectRole +from src.database.models import Project, ProjectRole def db_remove_student_project(db: Session, project: Project, student_id: int): @@ -16,7 +16,8 @@ def db_add_student_project(db: Session, project: Project, student_id: int, skill db.commit() -def db_change_project_role(db: Session, project: Project, student_id: int, skill_id: int): +def db_change_project_role(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): pr = db.query(ProjectRole).where(ProjectRole.student_id == student_id and ProjectRole.project == project).one() + pr.drafter_id = drafter_id pr.skill_id = skill_id db.commit() diff --git a/backend/tests/test_routers/test_editions/test_projects/__init__.py b/backend/tests/test_routers/test_editions/test_projects/__init__.py new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..fdc9f9b4f --- /dev/null +++ b/backend/tests/test_routers/test_editions/test_projects/test_projects.py @@ -0,0 +1,161 @@ +from json import dumps + +import pytest +from fastapi.testclient import TestClient +from sqlalchemy.orm import Session + +from src.app.logic.projects_students import logic_add_student_project +from src.database.models import Edition, Project, Student, Skill, User + + +def test_get_projects(database_session: Session, test_client: TestClient): + database_session.add(Edition(year=2022)) + project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) + database_session.add(project) + database_session.commit() + + response = test_client.get("/editions/1/projects") + json = response.json() + print(json) + + +def test_get_project(database_session: Session, test_client: TestClient): + database_session.add(Edition(year=2022)) + project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) + database_session.add(project) + database_session.commit() + + response = test_client.get("/editions/1/projects/1") + json = response.json() + print(json) + + +def test_delete_project(database_session: Session, test_client: TestClient): + database_session.add(Edition(year=2022)) + project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) + database_session.add(project) + database_session.commit() + + response = test_client.delete("/editions/1/projects/1") + print(response) + + +def test_create_project(database_session: Session, test_client: TestClient): + database_session.add(Edition(year=2022)) + # project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) + # database_session.add(project) + database_session.commit() + + response = \ + test_client.post("/editions/1/projects/", + json={"name": "test", + "number_of_students": 5, + "skills": [], "partners": [], "coaches": []}) + print(response) + response2 = test_client.get('/editions/1/projects') + json = response2.json() + print(json) + + +def test_patch_project(database_session: Session, test_client: TestClient): + database_session.add(Edition(year=2022)) + project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) + database_session.add(project) + database_session.commit() + + response = \ + test_client.patch("/editions/1/projects/1", + json={"name": "patched", + "number_of_students": 5, + "skills": [], "partners": [], "coaches": []}) + print(response) + response2 = test_client.get('/editions/1/projects') + json = response2.json() + print(json) + + +def test_add_student_project(database_session: Session, test_client: TestClient): + database_session.add(Edition(year=2022)) + project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) + student = Student(student_id=1, first_name="test", last_name="person", preferred_name="test", + email_address="a@b.com", + alumni=False, edition_id=1) + skill = Skill(skill_id=1, name="test_skill") + skill = Skill(skill_id=2, name="test_skill2") + user = User(user_id=1, name="testuser", email="b@c.com") + + database_session.add(project) + database_session.commit() + + print("test_add") + resp = test_client.post("/editions/1/projects/1/students/1", json={"skill_id": 1, "drafter_id": 1}) + print(resp) + response2 = test_client.get('/editions/1/projects') + json = response2.json() + print(json) + + +def test_change_student_project(database_session: Session, test_client: TestClient): + database_session.add(Edition(year=2022)) + project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) + student = Student(student_id=1, first_name="test", last_name="person", preferred_name="test", + email_address="a@b.com", + alumni=False, edition_id=1) + skill = Skill(skill_id=1, name="test_skill") + skill = Skill(skill_id=2, name="test_skill2") + user = User(user_id=1, name="testuser", email="b@c.com") + + database_session.add(project) + database_session.commit() + + print("test_change:") + logic_add_student_project(database_session, project, 1, 1, 1) + resp1 = test_client.patch("/editions/1/projects/1/students/1", json={"skill_id": 2, "drafter_id": 2}) + print(resp1) + response2 = test_client.get('/editions/1/projects') + json = response2.json() + print(json) + + +def test_delete_student_project(database_session: Session, test_client: TestClient): + database_session.add(Edition(year=2022)) + project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) + student = Student(student_id=1, first_name="test", last_name="person", preferred_name="test", + email_address="a@b.com", + alumni=False, edition_id=1) + skill = Skill(skill_id=1, name="test_skill") + user = User(user_id=1, name="testuser", email="b@c.com") + + database_session.add(project) + database_session.commit() + + logic_add_student_project(database_session, project, 1, 1, 1) + test_client.delete("/editions/1/projects/1/students/1") + response2 = test_client.get('/editions/1/projects') + json = response2.json() + print(json) + + +def test_get_conflicts(database_session: Session, test_client: TestClient): + database_session.add(Edition(year=2022)) + project = Project(name="project", edition_id=1, project_id=1, number_of_students=1) + project2 = Project(name="project2", edition_id=1, project_id=2, number_of_students=1) + student = Student(student_id=1, first_name="test", last_name="person", preferred_name="test", + email_address="a@b.com", + alumni=False, edition_id=1) + skill = Skill(skill_id=1, name="test_skill") + skill2 = Skill(skill_id=2, name="test_skill2") + user = User(user_id=1, name="testuser", email="b@c.com") + database_session.add(project) + database_session.add(project2) + database_session.add(student) + database_session.add(skill) + database_session.add(skill2) + database_session.add(user) + database_session.commit() + + logic_add_student_project(database_session, project, 1, 1, 1) + logic_add_student_project(database_session, project2, 1, 2, 1) + response2 = test_client.get("/editions/1/projects/conflicts") + json = response2.json() + print(json) From 216af25dcee06c2d0f8996ec73af6be14ceee207 Mon Sep 17 00:00:00 2001 From: beguille Date: Tue, 15 Mar 2022 21:59:44 +0100 Subject: [PATCH 054/536] added some tests --- .../test_projects/test_projects.py | 82 ++++++++++--------- 1 file changed, 42 insertions(+), 40 deletions(-) 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 fdc9f9b4f..a2db6fecc 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 @@ -3,6 +3,7 @@ import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session +from starlette import status from src.app.logic.projects_students import logic_add_student_project from src.database.models import Edition, Project, Student, Skill, User @@ -16,7 +17,9 @@ def test_get_projects(database_session: Session, test_client: TestClient): response = test_client.get("/editions/1/projects") json = response.json() - print(json) + + assert len(json['projects']) == 1 + assert json['projects'][0]['name'] == "project" def test_get_project(database_session: Session, test_client: TestClient): @@ -27,7 +30,8 @@ def test_get_project(database_session: Session, test_client: TestClient): response = test_client.get("/editions/1/projects/1") json = response.json() - print(json) + + assert json['name'] == 'project' def test_delete_project(database_session: Session, test_client: TestClient): @@ -37,13 +41,12 @@ def test_delete_project(database_session: Session, test_client: TestClient): database_session.commit() response = test_client.delete("/editions/1/projects/1") - print(response) + + assert response.status_code == status.HTTP_204_NO_CONTENT def test_create_project(database_session: Session, test_client: TestClient): database_session.add(Edition(year=2022)) - # project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) - # database_session.add(project) database_session.commit() response = \ @@ -51,10 +54,14 @@ def test_create_project(database_session: Session, test_client: TestClient): json={"name": "test", "number_of_students": 5, "skills": [], "partners": [], "coaches": []}) - print(response) + + assert response.status_code == status.HTTP_201_CREATED + response2 = test_client.get('/editions/1/projects') json = response2.json() - print(json) + + assert len(json['projects']) == 1 + assert json['projects'][0]['name'] == "test" def test_patch_project(database_session: Session, test_client: TestClient): @@ -68,72 +75,65 @@ def test_patch_project(database_session: Session, test_client: TestClient): json={"name": "patched", "number_of_students": 5, "skills": [], "partners": [], "coaches": []}) - print(response) + assert response.status_code == status.HTTP_204_NO_CONTENT + response2 = test_client.get('/editions/1/projects') json = response2.json() - print(json) + + assert len(json['projects']) == 1 + assert json['projects'][0]['name'] == 'patched' def test_add_student_project(database_session: Session, test_client: TestClient): database_session.add(Edition(year=2022)) project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) - student = Student(student_id=1, first_name="test", last_name="person", preferred_name="test", - email_address="a@b.com", - alumni=False, edition_id=1) - skill = Skill(skill_id=1, name="test_skill") - skill = Skill(skill_id=2, name="test_skill2") - user = User(user_id=1, name="testuser", email="b@c.com") - database_session.add(project) database_session.commit() - print("test_add") resp = test_client.post("/editions/1/projects/1/students/1", json={"skill_id": 1, "drafter_id": 1}) - print(resp) + + assert resp.status_code == status.HTTP_201_CREATED + response2 = test_client.get('/editions/1/projects') json = response2.json() - print(json) + + assert len(json['projects'][0]['projectRoles']) == 1 + assert json['projects'][0]['projectRoles'][0]['skillId'] == 1 def test_change_student_project(database_session: Session, test_client: TestClient): database_session.add(Edition(year=2022)) project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) - student = Student(student_id=1, first_name="test", last_name="person", preferred_name="test", - email_address="a@b.com", - alumni=False, edition_id=1) - skill = Skill(skill_id=1, name="test_skill") - skill = Skill(skill_id=2, name="test_skill2") - user = User(user_id=1, name="testuser", email="b@c.com") - database_session.add(project) database_session.commit() - print("test_change:") logic_add_student_project(database_session, project, 1, 1, 1) resp1 = test_client.patch("/editions/1/projects/1/students/1", json={"skill_id": 2, "drafter_id": 2}) - print(resp1) + + assert resp1.status_code == status.HTTP_204_NO_CONTENT + response2 = test_client.get('/editions/1/projects') json = response2.json() - print(json) + + assert len(json['projects'][0]['projectRoles']) == 1 + assert json['projects'][0]['projectRoles'][0]['skillId'] == 2 def test_delete_student_project(database_session: Session, test_client: TestClient): database_session.add(Edition(year=2022)) project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) - student = Student(student_id=1, first_name="test", last_name="person", preferred_name="test", - email_address="a@b.com", - alumni=False, edition_id=1) - skill = Skill(skill_id=1, name="test_skill") - user = User(user_id=1, name="testuser", email="b@c.com") - database_session.add(project) database_session.commit() logic_add_student_project(database_session, project, 1, 1, 1) - test_client.delete("/editions/1/projects/1/students/1") + resp = test_client.delete("/editions/1/projects/1/students/1") + + assert resp.status_code == status.HTTP_204_NO_CONTENT + response2 = test_client.get('/editions/1/projects') json = response2.json() - print(json) + + assert len(json['projects'][0]['projectRoles']) == 0 def test_get_conflicts(database_session: Session, test_client: TestClient): @@ -156,6 +156,8 @@ def test_get_conflicts(database_session: Session, test_client: TestClient): logic_add_student_project(database_session, project, 1, 1, 1) logic_add_student_project(database_session, project2, 1, 2, 1) - response2 = test_client.get("/editions/1/projects/conflicts") - json = response2.json() - print(json) + response = test_client.get("/editions/1/projects/conflicts") + json = response.json() + + assert len(json['students']) == 1 + assert json['students'][0]['studentId'] == 1 From 6bc087dbd7e9779b7c15b993394a67c56e6fd07d Mon Sep 17 00:00:00 2001 From: beguille Date: Wed, 16 Mar 2022 16:23:24 +0100 Subject: [PATCH 055/536] closes #88, closes #89, closes #91, closes #92, closes #93, closes#94, closes #95, fixed get_conflicts and added extra tests --- backend/src/app/logic/projects.py | 8 +- .../app/routers/editions/projects/projects.py | 5 +- backend/src/app/schemas/projects.py | 19 +--- backend/src/database/crud/projects.py | 31 ++++-- .../test_projects/test_projects.py | 100 +++++++++++++++++- 5 files changed, 133 insertions(+), 30 deletions(-) diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index 472935404..fe030b856 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session -from src.app.schemas.projects import ProjectList, Project, ConflictProjectList, ConflictProject, StudentList +from src.app.schemas.projects import ProjectList, Project, ConflictStudentList from src.database.crud.projects import db_get_all_projects, db_add_project, db_delete_project, \ db_patch_project, db_get_conflict_students from src.database.models import Edition @@ -25,6 +25,6 @@ def logic_patch_project(db: Session, project: Project, name: str, number_of_stud db_patch_project(db, project, name, number_of_students, skills, partners, coaches) -def logic_get_conflicts(db: Session, edition: Edition) -> StudentList: - conflicts = db_get_conflict_students(db,edition) - return StudentList(students=conflicts) +def logic_get_conflicts(db: Session, edition: Edition) -> ConflictStudentList: + conflicts = db_get_conflict_students(db, edition) + return ConflictStudentList(conflict_students=conflicts) diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index 7a9a7d099..b5513d6cd 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -6,7 +6,8 @@ from src.app.logic.projects import logic_get_project_list, logic_create_project, logic_delete_project, \ logic_patch_project, logic_get_conflicts from src.app.routers.tags import Tags -from src.app.schemas.projects import ProjectList, Project, StudentList, InputProject +from src.app.schemas.projects import ProjectList, Project, InputProject, \ + ConflictStudentList from src.app.utils.dependencies import get_edition, get_project from src.database.database import get_session from src.database.models import Edition @@ -36,7 +37,7 @@ async def create_project(input_project: InputProject, input_project.skills, input_project.partners, input_project.coaches) -@projects_router.get("/conflicts", response_model=StudentList) +@projects_router.get("/conflicts", response_model=ConflictStudentList) async def get_conflicts(db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): """ Get a list of all projects with conflicts, and the users that diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index 28e3c1227..b24ebe0ea 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -11,7 +11,6 @@ class User(CamelCaseModel): class Config: orm_mode = True - allow_population_by_field_name = True class Skill(CamelCaseModel): @@ -21,7 +20,6 @@ class Skill(CamelCaseModel): class Config: orm_mode = True - allow_population_by_field_name = True class Partner(CamelCaseModel): @@ -30,7 +28,6 @@ class Partner(CamelCaseModel): class Config: orm_mode = True - allow_population_by_field_name = True class ProjectRole(CamelCaseModel): @@ -43,7 +40,6 @@ class ProjectRole(CamelCaseModel): class Config: orm_mode = True - allow_population_by_field_name = True class Project(CamelCaseModel): @@ -59,7 +55,6 @@ class Project(CamelCaseModel): class Config: orm_mode = True - allow_population_by_field_name = True class Student(CamelCaseModel): @@ -76,23 +71,19 @@ class Student(CamelCaseModel): class Config: orm_mode = True - allow_population_by_field_name = True - - -class StudentList(CamelCaseModel): - students: list[Student] class ProjectList(CamelCaseModel): projects: list[Project] -class ConflictProject(Project): - conflicting_students = list[Student] +class ConflictStudent(CamelCaseModel): + student: Student + projects: list[Project] -class ConflictProjectList(CamelCaseModel): - conflict_projects = list[ConflictProject] +class ConflictStudentList(CamelCaseModel): + conflict_students: list[ConflictStudent] class InputProject(BaseModel): diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index 61374c03c..093ae2c81 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -1,6 +1,7 @@ from sqlalchemy.exc import NoResultFound from sqlalchemy.orm import Session +from src.app.schemas.projects import ConflictStudent from src.database.models import Project, Edition, Student, ProjectRole, Skill, User, Partner @@ -62,10 +63,28 @@ def db_patch_project(db: Session, project: Project, name: str, number_of_student db.commit() -def db_get_conflict_students(db: Session, edition: Edition) -> list[Student]: +# def db_get_conflict_students(db: Session, edition: Edition) -> list[Student]: +# students = db.query(Student).where(Student.edition == edition).all() +# conflicts = [] +# for s in students: +# if len(s.project_roles) > 1: +# conflicts.append(s) +# return conflicts + + +def db_get_conflict_students(db: Session, edition: Edition) -> list[ConflictStudent]: + students = db.query(Student).where(Student.edition == edition).all() - conflicts = [] - for s in students: - if len(s.project_roles) > 1: - conflicts.append(s) - return conflicts + 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) + cp = ConflictStudent(student=student, projects=projs) + conflict_students.append(cp) + return conflict_students + 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 a2db6fecc..18f8c9378 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 @@ -45,6 +45,15 @@ def test_delete_project(database_session: Session, test_client: TestClient): assert response.status_code == status.HTTP_204_NO_CONTENT +def test_delete_no_projects(database_session: Session, test_client: TestClient): + database_session.add(Edition(year=2022)) + database_session.commit() + + response = test_client.delete("/editions/1/projects/1") + + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_create_project(database_session: Session, test_client: TestClient): database_session.add(Edition(year=2022)) database_session.commit() @@ -64,6 +73,25 @@ def test_create_project(database_session: Session, test_client: TestClient): assert json['projects'][0]['name'] == "test" +def test_create_wrong_project(database_session: Session, test_client: TestClient): + database_session.add(Edition(year=2022)) + database_session.commit() + + response = \ + test_client.post("/editions/1/projects/", + # project has no name + json={ + "number_of_students": 5, + "skills": [], "partners": [], "coaches": []}) + + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + response2 = test_client.get('/editions/1/projects') + json = response2.json() + + assert len(json['projects']) == 0 + + def test_patch_project(database_session: Session, test_client: TestClient): database_session.add(Edition(year=2022)) project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) @@ -84,6 +112,25 @@ def test_patch_project(database_session: Session, test_client: TestClient): assert json['projects'][0]['name'] == 'patched' +def test_patch_wrong_project(database_session: Session, test_client: TestClient): + database_session.add(Edition(year=2022)) + project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) + database_session.add(project) + database_session.commit() + + response = \ + test_client.patch("/editions/1/projects/1", + json={"name": "patched", + "skills": [], "partners": [], "coaches": []}) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + response2 = test_client.get('/editions/1/projects') + json = response2.json() + + assert len(json['projects']) == 1 + assert json['projects'][0]['name'] == 'project' + + def test_add_student_project(database_session: Session, test_client: TestClient): database_session.add(Edition(year=2022)) project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) @@ -101,6 +148,22 @@ def test_add_student_project(database_session: Session, test_client: TestClient) assert json['projects'][0]['projectRoles'][0]['skillId'] == 1 +def test_add_wrong_student_project(database_session: Session, test_client: TestClient): + database_session.add(Edition(year=2022)) + project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) + database_session.add(project) + database_session.commit() + + resp = test_client.post("/editions/1/projects/1/students/1", json={"drafter_id": 1}) + + assert resp.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + response2 = test_client.get('/editions/1/projects') + json = response2.json() + + assert len(json['projects'][0]['projectRoles']) == 0 + + def test_change_student_project(database_session: Session, test_client: TestClient): database_session.add(Edition(year=2022)) project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) @@ -119,6 +182,24 @@ def test_change_student_project(database_session: Session, test_client: TestClie assert json['projects'][0]['projectRoles'][0]['skillId'] == 2 +def test_change_wrong_student_project(database_session: Session, test_client: TestClient): + database_session.add(Edition(year=2022)) + project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) + database_session.add(project) + database_session.commit() + + logic_add_student_project(database_session, project, 1, 1, 1) + resp1 = test_client.patch("/editions/1/projects/1/students/1", json={"skill_id": 2}) + + assert resp1.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + response2 = test_client.get('/editions/1/projects') + json = response2.json() + + assert len(json['projects'][0]['projectRoles']) == 1 + assert json['projects'][0]['projectRoles'][0]['skillId'] == 1 + + def test_delete_student_project(database_session: Session, test_client: TestClient): database_session.add(Edition(year=2022)) project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) @@ -136,10 +217,21 @@ def test_delete_student_project(database_session: Session, test_client: TestClie assert len(json['projects'][0]['projectRoles']) == 0 +def test_delete_student_project_empty(database_session: Session, test_client: TestClient): + database_session.add(Edition(year=2022)) + project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) + database_session.add(project) + database_session.commit() + + resp = test_client.delete("/editions/1/projects/1/students/1") + + assert resp.status_code == status.HTTP_404_NOT_FOUND + + def test_get_conflicts(database_session: Session, test_client: TestClient): database_session.add(Edition(year=2022)) project = Project(name="project", edition_id=1, project_id=1, number_of_students=1) - project2 = Project(name="project2", edition_id=1, project_id=2, number_of_students=1) + project2 = Project(name="project2", edition_id=1, project_id=3, number_of_students=1) student = Student(student_id=1, first_name="test", last_name="person", preferred_name="test", email_address="a@b.com", alumni=False, edition_id=1) @@ -158,6 +250,6 @@ def test_get_conflicts(database_session: Session, test_client: TestClient): logic_add_student_project(database_session, project2, 1, 2, 1) response = test_client.get("/editions/1/projects/conflicts") json = response.json() - - assert len(json['students']) == 1 - assert json['students'][0]['studentId'] == 1 + assert len(json['conflictStudents']) == 1 + assert json['conflictStudents'][0]['student']['studentId'] == 1 + assert len(json['conflictStudents'][0]['projects']) == 2 From dd2a61f86f4dcd9d81dff573128cbda0d0406216 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 18 Mar 2022 19:00:34 +0100 Subject: [PATCH 056/536] read & create from /suggestions + tests --- backend/src/app/logic/suggestions.py | 9 ++-- .../students/suggestions/suggestions.py | 18 +++---- backend/src/app/schemas/suggestion.py | 34 +++++++++++++- backend/src/app/utils/dependencies.py | 7 ++- backend/src/database/crud/students.py | 5 ++ .../test_database/test_crud/test_students.py | 18 +++++++ .../test_crud/test_suggestions.py | 2 +- backend/tests/test_logic/test_suggestions.py | 17 +++++++ .../test_suggestions/test_suggestions.py | 47 +++++++++++++++++-- 9 files changed, 137 insertions(+), 20 deletions(-) create mode 100644 backend/src/database/crud/students.py create mode 100644 backend/tests/test_database/test_crud/test_students.py create mode 100644 backend/tests/test_logic/test_suggestions.py diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py index d8a7d372e..db5c9047a 100644 --- a/backend/src/app/logic/suggestions.py +++ b/backend/src/app/logic/suggestions.py @@ -2,11 +2,14 @@ from src.app.schemas.suggestion import NewSuggestion from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student -from src.database.models import Suggestion, User +from src.database.models import User +from src.app.schemas.suggestion import SuggestionListResponse def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, user: User, student_id: int): create_suggestion(db, user.user_id, student_id, new_suggestion.suggestion, new_suggestion.argumentation) -def all_suggestions_of_student(db: Session, student_id: int) -> list[Suggestion]: - return get_suggestions_of_student(db, student_id) +def all_suggestions_of_student(db: Session, student_id: int) -> SuggestionListResponse: + suggestions_orm = get_suggestions_of_student(db, student_id) + #return suggestions_orm + return SuggestionListResponse(suggestions=suggestions_orm) #def get_suggestions(db: ) \ No newline at end of file diff --git a/backend/src/app/routers/editions/students/suggestions/suggestions.py b/backend/src/app/routers/editions/students/suggestions/suggestions.py index d8912b343..cdd952963 100644 --- a/backend/src/app/routers/editions/students/suggestions/suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/suggestions.py @@ -3,30 +3,30 @@ from starlette import status from src.app.routers.tags import Tags -from src.app.utils.dependencies import get_edition, require_authorization +from src.app.utils.dependencies import require_authorization, get_student from src.database.database import get_session -from src.database.models import Edition, Student, User -from src.app.logic.suggestions import make_new_suggestion, get_suggestions_of_student -from src.app.schemas.suggestion import NewSuggestion +from src.database.models import Student, User +from src.app.logic.suggestions import make_new_suggestion, all_suggestions_of_student +from src.app.schemas.suggestion import NewSuggestion, SuggestionListResponse students_suggestions_router = APIRouter(prefix="/suggestions", tags=[Tags.STUDENTS]) @students_suggestions_router.post("/", status_code=status.HTTP_201_CREATED) -async def create_suggestion(student_id: int, new_suggestion: NewSuggestion, db: Session = Depends(get_session), user: User = Depends(require_authorization)): +async def create_suggestion(new_suggestion: NewSuggestion, student: Student = Depends(get_student), db: Session = Depends(get_session), user: User = Depends(require_authorization)): """ Make a suggestion about a student. """ - make_new_suggestion(db, new_suggestion, user, student_id) + make_new_suggestion(db, new_suggestion, user, student.student_id) -@students_suggestions_router.get("/{suggestion_id}", dependencies=[Depends(require_authorization)], status_code=status.HTTP_200_OK) -async def get_suggestion(student_id: int, db: Session = Depends(get_session)): +@students_suggestions_router.get("/", dependencies=[Depends(require_authorization)], status_code=status.HTTP_200_OK, response_model=SuggestionListResponse) +async def get_suggestion(student: Student = Depends(get_student), db: Session = Depends(get_session)): """ Get all suggestions of a student. """ - return get_suggestions_of_student(db, student_id) + return all_suggestions_of_student(db, student.student_id) @students_suggestions_router.put("/{suggestion_id}", dependencies=[Depends(require_authorization)]) diff --git a/backend/src/app/schemas/suggestion.py b/backend/src/app/schemas/suggestion.py index bead93d0f..58f259640 100644 --- a/backend/src/app/schemas/suggestion.py +++ b/backend/src/app/schemas/suggestion.py @@ -4,4 +4,36 @@ class NewSuggestion(CamelCaseModel): """The fields of a suggestion""" suggestion: DecisionEnum - argumentation: str \ No newline at end of file + argumentation: str + +class User(CamelCaseModel): + """ + Model to represent a Coach + Sent as a response to API /GET requests + """ + user_id: int + name: str + email: str + + class Config: + orm_mode = True + +class Suggestion(CamelCaseModel): + """ + Model to represent a Suggestion + Sent as a response to API /GET requests + """ + + suggestion_id: int + coach : User + suggestion: DecisionEnum + argumentation: str + + class Config: + orm_mode = True + +class SuggestionListResponse(CamelCaseModel): + """ + A list of suggestions models + """ + suggestions: list[Suggestion] \ No newline at end of file diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index 84f1929c8..88dd3b9f1 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -10,7 +10,8 @@ from src.database.crud.editions import get_edition_by_id from src.database.crud.invites import get_invite_link_by_uuid from src.database.database import get_session -from src.database.models import Edition, InviteLink, User +from src.database.models import Edition, InviteLink, Student, User +from src.database.crud.students import get_student_by_id # TODO: Might be nice to use a more descriptive year number here than primary id. @@ -18,6 +19,10 @@ def get_edition(edition_id: int, database: Session = Depends(get_session)) -> Ed """Get an edition from the database, given the id in the path""" return get_edition_by_id(database, edition_id) +def get_student(student_id: int, database: Session = Depends(get_session)) -> Student: + """Get the student from the database, given the id in the path""" + return get_student_by_id(database, student_id) + oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login/token") diff --git a/backend/src/database/crud/students.py b/backend/src/database/crud/students.py new file mode 100644 index 000000000..3b10af6a2 --- /dev/null +++ b/backend/src/database/crud/students.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import Session +from src.database.models import Student + +def get_student_by_id(database_session: Session, student_id: int) -> Student: + return database_session.query(Student).where(Student.student_id == student_id).one() diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py new file mode 100644 index 000000000..221d92548 --- /dev/null +++ b/backend/tests/test_database/test_crud/test_students.py @@ -0,0 +1,18 @@ +import pytest +from sqlalchemy.orm import Session +from tests.fill_database import fill_database +from src.database.models import Student +from src.database.crud.students import get_student_by_id +from sqlalchemy.orm.exc import NoResultFound + +def test_get_student_by_id(database_session: Session): + fill_database(database_session) + student: Student = get_student_by_id(database_session, 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_session: Session): + with pytest.raises(NoResultFound): + get_student_by_id(database_session, 5) \ No newline at end of file diff --git a/backend/tests/test_database/test_crud/test_suggestions.py b/backend/tests/test_database/test_crud/test_suggestions.py index d823c6e39..6d6139c0f 100644 --- a/backend/tests/test_database/test_crud/test_suggestions.py +++ b/backend/tests/test_database/test_crud/test_suggestions.py @@ -76,4 +76,4 @@ def test_get_suggestions_of_student(database_session: Session): assert len(suggestions_student) == 2 assert suggestions_student[0].student == student - assert suggestions_student[1].student == student \ No newline at end of file + assert suggestions_student[1].student == student diff --git a/backend/tests/test_logic/test_suggestions.py b/backend/tests/test_logic/test_suggestions.py new file mode 100644 index 000000000..e6d5950a1 --- /dev/null +++ b/backend/tests/test_logic/test_suggestions.py @@ -0,0 +1,17 @@ +from sqlalchemy.orm import Session + +from src.app.logic.suggestions import all_suggestions_of_student +from src.database.models import User +from tests.fill_database import fill_database +from src.database.crud.suggestions import create_suggestion +from src.database.enums import DecisionEnum +from src.app.schemas.suggestion import SuggestionListResponse + +def test_all_suggestions_of_student(database_session: Session): + """Test if I get all suggestions of a student""" + fill_database(database_session) + user: User = database_session.query(User).where(User.email == "coach1@noutlook.be") + create_suggestion(database_session, 2, 1, DecisionEnum.YES, "Idk if it's good student") + suggestions: SuggestionListResponse = all_suggestions_of_student(database_session, 1) + print(suggestions) + assert True 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 e9b5cb7aa..854364e8a 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 @@ -3,10 +3,11 @@ from starlette.testclient import TestClient from tests.fill_database import fill_database from src.database.models import Suggestion, Student, User -from src.app.utils.dependencies import get_current_active_user def test_new_suggestion(database_session: Session, test_client: TestClient): + """Tests a new sugesstion""" + fill_database(database_session) email = "coach1@noutlook.be" password = "wachtwoord" @@ -21,11 +22,39 @@ def test_new_suggestion(database_session: Session, test_client: TestClient): assert len(suggestions) > 0 def test_new_suggestion_not_authorized(database_session: Session, test_client: TestClient): + """Tests when not authorized you can't add a new suggestion""" + + fill_database(database_session) assert test_client.post("/editions/1/students/29/suggestions/", json={"suggestion":1, "argumentation":"test"}).status_code == status.HTTP_401_UNAUTHORIZED suggestions: list[Suggestion] = database_session.query(Suggestion).where(Suggestion.student_id==29).all() assert len(suggestions) == 0 + +def test_get_suggestions_of_student_not_authorized(database_session: Session, test_client: TestClient): + """Tests if you don't have the right access, you get the right HTTP code""" + + assert test_client.get("/editions/1/students/29/suggestions/", headers={"Authorization": "auth"}, json={"suggestion":1, "argumentation":"Ja"}).status_code == status.HTTP_401_UNAUTHORIZED + + +def test_get_suggestions_of_ghost(database_session: Session, test_client: TestClient): + """Tests if the student don't exist, you get a 404""" + + fill_database(database_session) + email = "coach1@noutlook.be" + password = "wachtwoord" + form = { + "username": email, + "password": password + } + d = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer "+d + res = test_client.get("/editions/1/students/9000/suggestions/", headers={"Authorization": auth}) + assert res.status_code == status.HTTP_404_NOT_FOUND + + def test_get_suggestions_of_student(database_session: Session, test_client: TestClient): + """Tests to get the suggestions of a student""" + fill_database(database_session) form = { "username": "coach1@noutlook.be", @@ -33,16 +62,24 @@ def test_get_suggestions_of_student(database_session: Session, test_client: Test } d = test_client.post("/login/token", data=form).json()["accessToken"] auth = "Bearer "+d - assert test_client.post("/editions/1/students/29/suggestions/", headers={"Authorization": auth}, json={"suggestion":1, "argumentation":"test"}).status_code == status.HTTP_201_CREATED + assert test_client.post("/editions/1/students/29/suggestions/", headers={"Authorization": auth}, json={"suggestion":1, "argumentation":"Ja"}).status_code == status.HTTP_201_CREATED form = { "username": "admin@ngmail.com", "password": "wachtwoord" } d = test_client.post("/login/token", data=form).json()["accessToken"] auth = "Bearer "+d - assert test_client.post("/editions/1/students/29/suggestions/", headers={"Authorization": auth}, json={"suggestion":1, "argumentation":"test"}).status_code == status.HTTP_201_CREATED - - + assert test_client.post("/editions/1/students/29/suggestions/", headers={"Authorization": auth}, json={"suggestion":3, "argumentation":"Neen"}).status_code == status.HTTP_201_CREATED + res = test_client.get("/editions/1/students/29/suggestions/", headers={"Authorization": auth}) + assert res.status_code == status.HTTP_200_OK + res_json = res.json() + assert len(res_json["suggestions"]) == 2 + assert res_json["suggestions"][0]["coach"]["email"] == "coach1@noutlook.be" + assert res_json["suggestions"][0]["suggestion"] == 1 + assert res_json["suggestions"][0]["argumentation"] == "Ja" + assert res_json["suggestions"][1]["coach"]["email"] == "admin@ngmail.com" + assert res_json["suggestions"][1]["suggestion"] == 3 + assert res_json["suggestions"][1]["argumentation"] == "Neen" #, json={"suggestion":"OK", "argumentation":"test"} From 0489f6b1a557773f588d25cf6c0301aa5bbba7a1 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 18 Mar 2022 19:54:22 +0100 Subject: [PATCH 057/536] added extra tests --- backend/src/database/crud/suggestions.py | 5 ++- backend/tests/fill_database.py | 7 +++- .../test_crud/test_suggestions.py | 38 +++++++++++++++++-- backend/tests/test_logic/test_suggestions.py | 17 --------- 4 files changed, 44 insertions(+), 23 deletions(-) delete mode 100644 backend/tests/test_logic/test_suggestions.py diff --git a/backend/src/database/crud/suggestions.py b/backend/src/database/crud/suggestions.py index ca3f9f639..c0fb3978a 100644 --- a/backend/src/database/crud/suggestions.py +++ b/backend/src/database/crud/suggestions.py @@ -9,4 +9,7 @@ def create_suggestion(db: Session, user_id: int, student_id: int, decision: Deci db.commit() def get_suggestions_of_student(db: Session, student_id: int) -> list[Suggestion]: - return db.query(Suggestion).where(Suggestion.student_id == student_id).all() \ No newline at end of file + return db.query(Suggestion).where(Suggestion.student_id == student_id).all() + +def get_suggestion_by_id(db: Session, suggestion_id:int) -> Suggestion: + return db.query(Suggestion).where(Suggestion.suggestion_id == suggestion_id).one() \ No newline at end of file diff --git a/backend/tests/fill_database.py b/backend/tests/fill_database.py index 8e783d653..181638b8b 100644 --- a/backend/tests/fill_database.py +++ b/backend/tests/fill_database.py @@ -3,6 +3,7 @@ from src.database.models import * from src.database.enums import * from src.app.logic.security import get_password_hash +from src.database.enums import DecisionEnum from datetime import date def fill_database(db: Session): @@ -154,7 +155,7 @@ def fill_database(db: Session): db.add(partner3) db.commit() - #Project + # Project project1: Project = Project(name="project1", number_of_students=3, edition=edition, partners=[partner1]) project2: Project = Project(name="project2", number_of_students=6, edition=edition, partners=[partner2]) project3: Project = Project(name="project3", number_of_students=2, edition=edition, partners=[partner3]) @@ -165,3 +166,7 @@ def fill_database(db: Session): db.add(project4) db.commit() + # Suggestion + suggestion: Suggestion = Suggestion(student=student01,coach=coach1, argumentation="Good student", suggestion=DecisionEnum.YES) + db.add(suggestion) + db.commit() \ No newline at end of file diff --git a/backend/tests/test_database/test_crud/test_suggestions.py b/backend/tests/test_database/test_crud/test_suggestions.py index 6d6139c0f..ae4aeb074 100644 --- a/backend/tests/test_database/test_crud/test_suggestions.py +++ b/backend/tests/test_database/test_crud/test_suggestions.py @@ -4,7 +4,7 @@ from src.database.models import Suggestion, Student, User -from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student +from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student, get_suggestion_by_id from tests.fill_database import fill_database from src.database.enums import DecisionEnum @@ -16,7 +16,7 @@ def test_create_suggestion_yes(database_session: Session): create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") - suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user and Suggestion.student == student).first() + suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() assert suggestion.coach == user assert suggestion.student == student @@ -31,7 +31,7 @@ def test_create_suggestion_no(database_session: Session): create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.NO, "This is a not good student") - suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user and Suggestion.student == student).first() + suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() assert suggestion.coach == user assert suggestion.student == student @@ -46,13 +46,35 @@ def test_create_suggestion_maybe(database_session: Session): create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") - suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user and Suggestion.student == student).first() + suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() assert suggestion.coach == user assert suggestion.student == student assert suggestion.suggestion == DecisionEnum.MAYBE assert suggestion.argumentation == "Idk if it's good student" +def test_one_coach_two_students(database_session: Session): + fill_database(database_session) + + user: User = database_session.query(User).where(User.name == "coach1").one() + student1: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").one() + student2: Student = database_session.query(Student).where(Student.email_address == "sofia.haataja@example.com").one() + + create_suggestion(database_session, user.user_id, student1.student_id, DecisionEnum.YES, "This is a good student") + create_suggestion(database_session, user.user_id, student2.student_id, DecisionEnum.NO, "This is a not good student") + + suggestion1: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student1.student_id).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_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student2.student_id).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_session: Session): fill_database(database_session) @@ -77,3 +99,11 @@ def test_get_suggestions_of_student(database_session: Session): assert len(suggestions_student) == 2 assert suggestions_student[0].student == student assert suggestions_student[1].student == student + +def test_get_suggestion_by_id(database_session: Session): + fill_database(database_session) + suggestion: Suggestion = get_suggestion_by_id(database_session, 1) + assert suggestion.student_id == 1 + assert suggestion.coach_id == 2 + assert suggestion.suggestion == DecisionEnum.YES + assert suggestion.argumentation == "Good student" \ No newline at end of file diff --git a/backend/tests/test_logic/test_suggestions.py b/backend/tests/test_logic/test_suggestions.py deleted file mode 100644 index e6d5950a1..000000000 --- a/backend/tests/test_logic/test_suggestions.py +++ /dev/null @@ -1,17 +0,0 @@ -from sqlalchemy.orm import Session - -from src.app.logic.suggestions import all_suggestions_of_student -from src.database.models import User -from tests.fill_database import fill_database -from src.database.crud.suggestions import create_suggestion -from src.database.enums import DecisionEnum -from src.app.schemas.suggestion import SuggestionListResponse - -def test_all_suggestions_of_student(database_session: Session): - """Test if I get all suggestions of a student""" - fill_database(database_session) - user: User = database_session.query(User).where(User.email == "coach1@noutlook.be") - create_suggestion(database_session, 2, 1, DecisionEnum.YES, "Idk if it's good student") - suggestions: SuggestionListResponse = all_suggestions_of_student(database_session, 1) - print(suggestions) - assert True From c10f773f0cbcaac6e00a699f45f5aa3830be87ec Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 18 Mar 2022 20:15:03 +0100 Subject: [PATCH 058/536] added extra tests --- backend/tests/test_database/test_crud/test_suggestions.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_database/test_crud/test_suggestions.py b/backend/tests/test_database/test_crud/test_suggestions.py index ae4aeb074..778f428ae 100644 --- a/backend/tests/test_database/test_crud/test_suggestions.py +++ b/backend/tests/test_database/test_crud/test_suggestions.py @@ -7,6 +7,7 @@ from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student, get_suggestion_by_id from tests.fill_database import fill_database from src.database.enums import DecisionEnum +from sqlalchemy.orm.exc import NoResultFound def test_create_suggestion_yes(database_session: Session): fill_database(database_session) @@ -106,4 +107,8 @@ def test_get_suggestion_by_id(database_session: Session): assert suggestion.student_id == 1 assert suggestion.coach_id == 2 assert suggestion.suggestion == DecisionEnum.YES - assert suggestion.argumentation == "Good student" \ No newline at end of file + assert suggestion.argumentation == "Good student" + +def test_get_suggestion_by_id_non_existing(database_session: Session): + with pytest.raises(NoResultFound): + suggestion: Suggestion = get_suggestion_by_id(database_session, 1) \ No newline at end of file From 9562288ceec4a7da78faf3f90ac587f8c1a33c85 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 18 Mar 2022 21:04:21 +0100 Subject: [PATCH 059/536] crud delete & update suggestion --- backend/src/database/crud/suggestions.py | 10 +++++- .../test_crud/test_suggestions.py | 33 +++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/backend/src/database/crud/suggestions.py b/backend/src/database/crud/suggestions.py index c0fb3978a..d446ce323 100644 --- a/backend/src/database/crud/suggestions.py +++ b/backend/src/database/crud/suggestions.py @@ -12,4 +12,12 @@ def get_suggestions_of_student(db: Session, student_id: int) -> list[Suggestion] return db.query(Suggestion).where(Suggestion.student_id == student_id).all() def get_suggestion_by_id(db: Session, suggestion_id:int) -> Suggestion: - return db.query(Suggestion).where(Suggestion.suggestion_id == suggestion_id).one() \ No newline at end of file + return db.query(Suggestion).where(Suggestion.suggestion_id == suggestion_id).one() + +def delete_suggestion(db: Session, suggestion: Suggestion) -> None: + db.delete(suggestion) + +def update_suggestion(db: Session, suggestion: Suggestion, decision: DecisionEnum, argumentation: str) -> None: + suggestion.suggestion = decision + suggestion.argumentation = argumentation + db.commit() \ No newline at end of file diff --git a/backend/tests/test_database/test_crud/test_suggestions.py b/backend/tests/test_database/test_crud/test_suggestions.py index 778f428ae..48688bb3d 100644 --- a/backend/tests/test_database/test_crud/test_suggestions.py +++ b/backend/tests/test_database/test_crud/test_suggestions.py @@ -4,7 +4,7 @@ from src.database.models import Suggestion, Student, User -from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student, get_suggestion_by_id +from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student, get_suggestion_by_id, delete_suggestion, update_suggestion from tests.fill_database import fill_database from src.database.enums import DecisionEnum from sqlalchemy.orm.exc import NoResultFound @@ -111,4 +111,33 @@ def test_get_suggestion_by_id(database_session: Session): def test_get_suggestion_by_id_non_existing(database_session: Session): with pytest.raises(NoResultFound): - suggestion: Suggestion = get_suggestion_by_id(database_session, 1) \ No newline at end of file + suggestion: Suggestion = get_suggestion_by_id(database_session, 1) + +def test_delete_suggestion(database_session: Session): + fill_database(database_session) + + user: User = database_session.query(User).where(User.name == "coach1").first() + student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() + + create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") + suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + + delete_suggestion(database_session, suggestion) + + suggestions: list[Suggestion] = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).all() + assert len(suggestions) == 0 + +def test_update_suggestion(database_session: Session): + fill_database(database_session) + + user: User = database_session.query(User).where(User.name == "coach1").first() + student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() + + create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") + suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + + update_suggestion(database_session, suggestion, DecisionEnum.NO, "Not that good student") + + new_suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + assert suggestion.suggestion == DecisionEnum.NO + assert suggestion.argumentation == "Not that good student" \ No newline at end of file From f50f19ec685ac88e93f396c237895996b32279a1 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 18 Mar 2022 21:56:04 +0100 Subject: [PATCH 060/536] about #85 --- backend/src/app/logic/suggestions.py | 12 ++++--- .../students/suggestions/suggestions.py | 30 ++++++++--------- backend/src/app/utils/dependencies.py | 7 +++- .../test_suggestions/test_suggestions.py | 32 ++++++++++++++----- 4 files changed, 50 insertions(+), 31 deletions(-) diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py index db5c9047a..c1999082b 100644 --- a/backend/src/app/logic/suggestions.py +++ b/backend/src/app/logic/suggestions.py @@ -1,15 +1,17 @@ from sqlalchemy.orm import Session from src.app.schemas.suggestion import NewSuggestion -from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student -from src.database.models import User +from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student, delete_suggestion +from src.database.models import Suggestion, User from src.app.schemas.suggestion import SuggestionListResponse -def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, user: User, student_id: int): +def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, user: User, student_id: int) -> None: create_suggestion(db, user.user_id, student_id, new_suggestion.suggestion, new_suggestion.argumentation) def all_suggestions_of_student(db: Session, student_id: int) -> SuggestionListResponse: suggestions_orm = get_suggestions_of_student(db, student_id) - #return suggestions_orm return SuggestionListResponse(suggestions=suggestions_orm) -#def get_suggestions(db: ) \ No newline at end of file + +def remove_suggestion(db: Session, suggestion: Suggestion, user: User) -> None: + if(user.admin): + delete_suggestion(db, suggestion) \ No newline at end of file diff --git a/backend/src/app/routers/editions/students/suggestions/suggestions.py b/backend/src/app/routers/editions/students/suggestions/suggestions.py index cdd952963..054966833 100644 --- a/backend/src/app/routers/editions/students/suggestions/suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/suggestions.py @@ -3,10 +3,10 @@ from starlette import status from src.app.routers.tags import Tags -from src.app.utils.dependencies import require_authorization, get_student +from src.app.utils.dependencies import require_authorization, get_student, get_suggestion from src.database.database import get_session -from src.database.models import Student, User -from src.app.logic.suggestions import make_new_suggestion, all_suggestions_of_student +from src.database.models import Student, User, Suggestion +from src.app.logic.suggestions import make_new_suggestion, all_suggestions_of_student, remove_suggestion from src.app.schemas.suggestion import NewSuggestion, SuggestionListResponse @@ -14,32 +14,28 @@ @students_suggestions_router.post("/", status_code=status.HTTP_201_CREATED) -async def create_suggestion(new_suggestion: NewSuggestion, student: Student = Depends(get_student), db: Session = Depends(get_session), user: User = Depends(require_authorization)): +async def create_suggestion(new_suggestion: NewSuggestion, student: Student = Depends(get_student), db: Session = Depends(get_session), user: User = Depends(require_authorization)): """ Make a suggestion about a student. """ make_new_suggestion(db, new_suggestion, user, student.student_id) - -@students_suggestions_router.get("/", dependencies=[Depends(require_authorization)], status_code=status.HTTP_200_OK, response_model=SuggestionListResponse) -async def get_suggestion(student: Student = Depends(get_student), db: Session = Depends(get_session)): +@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_authorization), suggestion: Suggestion = Depends(get_suggestion)): """ - Get all suggestions of a student. + Delete a suggestion you made about a student. """ - return all_suggestions_of_student(db, student.student_id) - + remove_suggestion(db,suggestion,user) -@students_suggestions_router.put("/{suggestion_id}", dependencies=[Depends(require_authorization)]) +@students_suggestions_router.put("/{suggestion_id}") async def edit_suggestion(edition_id: int, student_id: int, suggestion_id: int): """ Edit a suggestion you made about a student. """ -@students_suggestions_router.delete("/{suggestion_id}", dependencies=[Depends(require_authorization)]) -async def delete_suggestion(edition_id: int, suggestion_id: int): +@students_suggestions_router.get("/", dependencies=[Depends(require_authorization)], status_code=status.HTTP_200_OK, response_model=SuggestionListResponse) +async def get_suggestion(student: Student = Depends(get_student), db: Session = Depends(get_session)): """ - Delete a suggestion you made about a student. + Get all suggestions of a student. """ - - -#user: NewUser, db: Session = Depends(get_session), edition: Edition = Depends(get_edition) \ No newline at end of file + return all_suggestions_of_student(db, student.student_id) diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index 88dd3b9f1..c4842a533 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -10,8 +10,9 @@ from src.database.crud.editions import get_edition_by_id from src.database.crud.invites import get_invite_link_by_uuid from src.database.database import get_session -from src.database.models import Edition, InviteLink, Student, User +from src.database.models import Edition, InviteLink, Student, Suggestion, User from src.database.crud.students import get_student_by_id +from src.database.crud.suggestions import get_suggestion_by_id # TODO: Might be nice to use a more descriptive year number here than primary id. @@ -23,6 +24,10 @@ def get_student(student_id: int, database: Session = Depends(get_session)) -> St """Get the student from the database, given the id in the path""" return get_student_by_id(database, student_id) +def get_suggestion(suggestion_id: int, database: Session = Depends(get_session)) -> Suggestion: + """Get the suggestion from the database, given the id in the path""" + return get_suggestion_by_id(database, suggestion_id) + oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login/token") 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 854364e8a..5c945d8fc 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 @@ -80,13 +80,29 @@ def test_get_suggestions_of_student(database_session: Session, test_client: Test assert res_json["suggestions"][1]["coach"]["email"] == "admin@ngmail.com" assert res_json["suggestions"][1]["suggestion"] == 3 assert res_json["suggestions"][1]["argumentation"] == "Neen" -#, json={"suggestion":"OK", "argumentation":"test"} +def test_delete_ghost_suggestion(database_session: Session, test_client: TestClient): + fill_database(database_session) + form = { + "username": "admin@ngmail.com", + "password": "wachtwoord" + } + d = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer "+d + assert test_client.delete("/editions/1/students/1/suggestions/8000", headers={"Authorization": auth}).status_code == status.HTTP_404_NOT_FOUND + +def test_delete_not_autorized(database_session: Session, test_client: TestClient): + fill_database(database_session) + assert test_client.delete("/editions/1/students/1/suggestions/8000", headers={"Authorization": "auth"}).status_code == status.HTTP_401_UNAUTHORIZED -""" -def test_ok(database_session: Session, test_client: TestClient): - database_session.add(Edition(year=2022)) - database_session.commit() - response = test_client.post("/editions/1/register/email", json={"name": "Joskes vermeulen","email": "jw@gmail.com", "pw": "test"}) - assert response.status_code == status. -""" \ No newline at end of file +def test_delete_suggestion_admin(database_session: Session, test_client: TestClient): + fill_database(database_session) + form = { + "username": "admin@ngmail.com", + "password": "wachtwoord" + } + d = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer "+d + assert test_client.delete("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}).status_code == status.HTTP_204_NO_CONTENT + suggestions: Suggestion = database_session.query(Suggestion).where(Suggestion.suggestion_id==1).all() + assert len(suggestions) == 0 From 2f14fc40c630539b427f30e7c8e32051ae7b2535 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 18 Mar 2022 22:02:18 +0100 Subject: [PATCH 061/536] about #86 --- backend/src/app/logic/suggestions.py | 7 +++++- .../test_suggestions/test_suggestions.py | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py index c1999082b..fe14020a5 100644 --- a/backend/src/app/logic/suggestions.py +++ b/backend/src/app/logic/suggestions.py @@ -4,6 +4,7 @@ from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student, delete_suggestion from src.database.models import Suggestion, User from src.app.schemas.suggestion import SuggestionListResponse +from src.app.exceptions.authentication import MissingPermissionsException def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, user: User, student_id: int) -> None: create_suggestion(db, user.user_id, student_id, new_suggestion.suggestion, new_suggestion.argumentation) @@ -14,4 +15,8 @@ def all_suggestions_of_student(db: Session, student_id: int) -> SuggestionListRe def remove_suggestion(db: Session, suggestion: Suggestion, user: User) -> None: if(user.admin): - delete_suggestion(db, suggestion) \ No newline at end of file + delete_suggestion(db, suggestion) + elif(suggestion.coach == user): + delete_suggestion(db, suggestion) + else: + raise MissingPermissionsException \ No newline at end of file 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 5c945d8fc..7b709e119 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 @@ -106,3 +106,27 @@ def test_delete_suggestion_admin(database_session: Session, test_client: TestCli assert test_client.delete("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}).status_code == status.HTTP_204_NO_CONTENT suggestions: Suggestion = database_session.query(Suggestion).where(Suggestion.suggestion_id==1).all() assert len(suggestions) == 0 + +def test_detele_suggestion_coach_their_review(database_session: Session, test_client: TestClient): + fill_database(database_session) + form = { + "username": "coach1@noutlook.be", + "password": "wachtwoord" + } + d = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer "+d + assert test_client.delete("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}).status_code == status.HTTP_204_NO_CONTENT + suggestions: Suggestion = database_session.query(Suggestion).where(Suggestion.suggestion_id==1).all() + assert len(suggestions) == 0 + +def test_detele_suggestion_coach_other_review(database_session: Session, test_client: TestClient): + fill_database(database_session) + form = { + "username": "coach2@noutlook.be", + "password": "wachtwoord" + } + d = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer "+d + assert test_client.delete("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}).status_code == status.HTTP_403_FORBIDDEN + suggestions: Suggestion = database_session.query(Suggestion).where(Suggestion.suggestion_id==1).all() + assert len(suggestions) == 1 \ No newline at end of file From f3c1840a33e007003f4aa723a43f9bad1a6dc217 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 18 Mar 2022 22:36:31 +0100 Subject: [PATCH 062/536] about #84 #83 --- backend/src/app/logic/suggestions.py | 12 +++- .../students/suggestions/suggestions.py | 7 ++- .../test_suggestions/test_suggestions.py | 60 ++++++++++++++++++- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py index fe14020a5..8cdea682e 100644 --- a/backend/src/app/logic/suggestions.py +++ b/backend/src/app/logic/suggestions.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import Session from src.app.schemas.suggestion import NewSuggestion -from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student, delete_suggestion +from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student, delete_suggestion, update_suggestion from src.database.models import Suggestion, User from src.app.schemas.suggestion import SuggestionListResponse from src.app.exceptions.authentication import MissingPermissionsException @@ -19,4 +19,12 @@ def remove_suggestion(db: Session, suggestion: Suggestion, user: User) -> None: elif(suggestion.coach == user): delete_suggestion(db, suggestion) else: - raise MissingPermissionsException \ No newline at end of file + raise MissingPermissionsException + +def change_suggestion(db: Session, new_suggestion: NewSuggestion, suggestion: Suggestion, user: User) -> None: + if(user.admin): + update_suggestion(db,suggestion,new_suggestion.suggestion, new_suggestion.argumentation) + elif(suggestion.coach == user): + update_suggestion(db,suggestion,new_suggestion.suggestion, new_suggestion.argumentation) + else: + raise MissingPermissionsException \ No newline at end of file diff --git a/backend/src/app/routers/editions/students/suggestions/suggestions.py b/backend/src/app/routers/editions/students/suggestions/suggestions.py index 054966833..36991f37c 100644 --- a/backend/src/app/routers/editions/students/suggestions/suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/suggestions.py @@ -6,7 +6,7 @@ from src.app.utils.dependencies import require_authorization, get_student, get_suggestion 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, remove_suggestion +from src.app.logic.suggestions import make_new_suggestion, all_suggestions_of_student, remove_suggestion, change_suggestion from src.app.schemas.suggestion import NewSuggestion, SuggestionListResponse @@ -27,11 +27,12 @@ async def delete_suggestion(db: Session = Depends(get_session), user: User = Dep """ remove_suggestion(db,suggestion,user) -@students_suggestions_router.put("/{suggestion_id}") -async def edit_suggestion(edition_id: int, student_id: int, suggestion_id: int): +@students_suggestions_router.put("/{suggestion_id}", status_code=status.HTTP_204_NO_CONTENT) +async def edit_suggestion(new_suggestion: NewSuggestion, student: Student = Depends(get_student), db: Session = Depends(get_session), user: User = Depends(require_authorization), suggestion: Suggestion = Depends(get_suggestion)): """ Edit a suggestion you made about a student. """ + change_suggestion(db,new_suggestion,suggestion,user) @students_suggestions_router.get("/", dependencies=[Depends(require_authorization)], status_code=status.HTTP_200_OK, response_model=SuggestionListResponse) async def get_suggestion(student: Student = Depends(get_student), db: Session = Depends(get_session)): 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 7b709e119..d4bda1510 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,6 +1,7 @@ from sqlalchemy.orm import Session from starlette import status from starlette.testclient import TestClient +from src.database.enums import DecisionEnum from tests.fill_database import fill_database from src.database.models import Suggestion, Student, User @@ -107,7 +108,7 @@ def test_delete_suggestion_admin(database_session: Session, test_client: TestCli suggestions: Suggestion = database_session.query(Suggestion).where(Suggestion.suggestion_id==1).all() assert len(suggestions) == 0 -def test_detele_suggestion_coach_their_review(database_session: Session, test_client: TestClient): +def test_delete_suggestion_coach_their_review(database_session: Session, test_client: TestClient): fill_database(database_session) form = { "username": "coach1@noutlook.be", @@ -119,7 +120,7 @@ def test_detele_suggestion_coach_their_review(database_session: Session, test_cl suggestions: Suggestion = database_session.query(Suggestion).where(Suggestion.suggestion_id==1).all() assert len(suggestions) == 0 -def test_detele_suggestion_coach_other_review(database_session: Session, test_client: TestClient): +def test_delete_suggestion_coach_other_review(database_session: Session, test_client: TestClient): fill_database(database_session) form = { "username": "coach2@noutlook.be", @@ -129,4 +130,57 @@ def test_detele_suggestion_coach_other_review(database_session: Session, test_cl auth = "Bearer "+d assert test_client.delete("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}).status_code == status.HTTP_403_FORBIDDEN suggestions: Suggestion = database_session.query(Suggestion).where(Suggestion.suggestion_id==1).all() - assert len(suggestions) == 1 \ No newline at end of file + assert len(suggestions) == 1 + +def test_update_ghost_suggestion(database_session: Session, test_client: TestClient): + fill_database(database_session) + form = { + "username": "admin@ngmail.com", + "password": "wachtwoord" + } + d = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer "+d + assert test_client.put("/editions/1/students/1/suggestions/8000", headers={"Authorization": auth}, json={"suggestion":1, "argumentation":"test"}).status_code == status.HTTP_404_NOT_FOUND + +def test_update_not_autorized(database_session: Session, test_client: TestClient): + fill_database(database_session) + assert test_client.put("/editions/1/students/1/suggestions/8000", headers={"Authorization": "auth"}, json={"suggestion":1, "argumentation":"test"}).status_code == status.HTTP_401_UNAUTHORIZED + +def test_update_suggestion_admin(database_session: Session, test_client: TestClient): + fill_database(database_session) + form = { + "username": "admin@ngmail.com", + "password": "wachtwoord" + } + d = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer "+d + assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}, json={"suggestion":3, "argumentation":"test"}).status_code == status.HTTP_204_NO_CONTENT + suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.suggestion_id==1).one() + assert suggestion.suggestion == DecisionEnum.NO + assert suggestion.argumentation == "test" + +def test_update_suggestion_coach_their_review(database_session: Session, test_client: TestClient): + fill_database(database_session) + form = { + "username": "coach1@noutlook.be", + "password": "wachtwoord" + } + d = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer "+d + assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}, json={"suggestion":3, "argumentation":"test"}).status_code == status.HTTP_204_NO_CONTENT + suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.suggestion_id==1).one() + assert suggestion.suggestion == DecisionEnum.NO + assert suggestion.argumentation == "test" + +def test_update_suggestion_coach_other_review(database_session: Session, test_client: TestClient): + fill_database(database_session) + form = { + "username": "coach2@noutlook.be", + "password": "wachtwoord" + } + d = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer "+d + assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}, json={"suggestion":3, "argumentation":"test"}).status_code == status.HTTP_403_FORBIDDEN + suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.suggestion_id==1).one() + assert suggestion.suggestion != DecisionEnum.NO + assert suggestion.argumentation != "test" \ No newline at end of file From fb221377165784d53c972f6c3d066df4603639a9 Mon Sep 17 00:00:00 2001 From: beguille Date: Sun, 20 Mar 2022 10:19:15 +0100 Subject: [PATCH 063/536] fixed requested changes --- backend/src/app/logic/projects.py | 6 +++--- .../app/routers/editions/projects/projects.py | 14 ++++++------- backend/src/app/schemas/projects.py | 4 ++++ backend/src/database/crud/projects.py | 20 ++++++------------- .../test_projects/test_projects.py | 1 + 5 files changed, 21 insertions(+), 24 deletions(-) diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index fe030b856..d98373d7d 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session -from src.app.schemas.projects import ProjectList, Project, ConflictStudentList +from src.app.schemas.projects import ProjectList, Project, ConflictStudentList, ProjectId from src.database.crud.projects import db_get_all_projects, db_add_project, db_delete_project, \ db_patch_project, db_get_conflict_students from src.database.models import Edition @@ -12,8 +12,8 @@ def logic_get_project_list(db: Session, edition: Edition) -> ProjectList: def logic_create_project(db: Session, edition: Edition, name: str, number_of_students: int, skills: [int], - partners: [str], coaches: [int]): - db_add_project(db, edition, name, number_of_students, skills, partners, coaches) + partners: [str], coaches: [int]) -> ProjectId: + return db_add_project(db, edition, name, number_of_students, skills, partners, coaches) def logic_delete_project(db: Session, project_id: int): diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index b5513d6cd..387596f5a 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -7,7 +7,7 @@ logic_patch_project, logic_get_conflicts from src.app.routers.tags import Tags from src.app.schemas.projects import ProjectList, Project, InputProject, \ - ConflictStudentList + ConflictStudentList, ProjectId from src.app.utils.dependencies import get_edition, get_project from src.database.database import get_session from src.database.models import Edition @@ -25,16 +25,16 @@ async def get_projects(db: Session = Depends(get_session), edition: Edition = De return logic_get_project_list(db, edition) -@projects_router.post("/", status_code=status.HTTP_201_CREATED, response_class=Response) +@projects_router.post("/", status_code=status.HTTP_201_CREATED, response_model=ProjectId) async def create_project(input_project: InputProject, db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): """ - Create a new project.users + Create a new project """ - logic_create_project(db, edition, - input_project.name, - input_project.number_of_students, - input_project.skills, input_project.partners, input_project.coaches) + return logic_create_project(db, edition, + input_project.name, + input_project.number_of_students, + input_project.skills, input_project.partners, input_project.coaches) @projects_router.get("/conflicts", response_model=ConflictStudentList) diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index b24ebe0ea..905a3aa65 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -86,6 +86,10 @@ class ConflictStudentList(CamelCaseModel): conflict_students: list[ConflictStudent] +class ProjectId(CamelCaseModel): + project_id: int + + class InputProject(BaseModel): name: str number_of_students: int diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index 093ae2c81..f8afaa127 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -1,7 +1,7 @@ from sqlalchemy.exc import NoResultFound from sqlalchemy.orm import Session -from src.app.schemas.projects import ConflictStudent +from src.app.schemas.projects import ConflictStudent, ProjectId from src.database.models import Project, Edition, Student, ProjectRole, Skill, User, Partner @@ -9,8 +9,8 @@ def db_get_all_projects(db: Session, edition: Edition) -> list[Project]: return db.query(Project).where(Project.edition == edition).all() -def db_add_project(db: Session, edition: Edition, name: str, number_of_students: int, skills: [int], - partners: [str], coaches: [int]): +def db_add_project(db: Session, edition: Edition, name: str, number_of_students: int, skills: list[int], + partners: list[str], coaches: list[int]) -> ProjectId: skills_obj = [db.query(Skill).where(Skill.skill_id == skill).one() for skill in skills] coaches_obj = [db.query(User).where(User.user_id) == coach for coach in coaches] partners_obj = [] @@ -26,6 +26,7 @@ def db_add_project(db: Session, edition: Edition, name: str, number_of_students: db.add(project) db.commit() + return ProjectId(project_id=project.project_id) def db_get_project(db: Session, project_id: int) -> Project: @@ -42,8 +43,8 @@ def db_delete_project(db: Session, project_id: int): db.commit() -def db_patch_project(db: Session, project: Project, name: str, number_of_students: int, skills: [int], - partners: [str], coaches: [int]): +def db_patch_project(db: Session, project: Project, name: str, number_of_students: int, skills: list[int], + partners: list[str], coaches: list[int]): skills_obj = [db.query(Skill).where(Skill.skill_id == skill).one() for skill in skills] coaches_obj = [db.query(User).where(User.user_id) == coach for coach in coaches] partners_obj = [] @@ -63,15 +64,6 @@ def db_patch_project(db: Session, project: Project, name: str, number_of_student db.commit() -# def db_get_conflict_students(db: Session, edition: Edition) -> list[Student]: -# students = db.query(Student).where(Student.edition == edition).all() -# conflicts = [] -# for s in students: -# if len(s.project_roles) > 1: -# conflicts.append(s) -# return conflicts - - def db_get_conflict_students(db: Session, edition: Edition) -> list[ConflictStudent]: students = db.query(Student).where(Student.edition == edition).all() 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 18f8c9378..77fa2f29e 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 @@ -65,6 +65,7 @@ def test_create_project(database_session: Session, test_client: TestClient): "skills": [], "partners": [], "coaches": []}) assert response.status_code == status.HTTP_201_CREATED + assert response.json()['projectId'] == 1 response2 = test_client.get('/editions/1/projects') json = response2.json() From 6e957b668da134526ce7bc29110d257ad19c227a Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Sun, 20 Mar 2022 10:45:57 +0100 Subject: [PATCH 064/536] use response model for new suggestion --- backend/src/app/logic/suggestions.py | 7 ++++--- .../editions/students/suggestions/suggestions.py | 6 +++--- backend/src/app/schemas/suggestion.py | 10 ++++++++-- backend/src/database/crud/suggestions.py | 3 ++- .../test_database/test_crud/test_suggestions.py | 12 +++++++++--- .../test_suggestions/test_suggestions.py | 9 +++++++-- 6 files changed, 33 insertions(+), 14 deletions(-) diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py index 8cdea682e..6a947b420 100644 --- a/backend/src/app/logic/suggestions.py +++ b/backend/src/app/logic/suggestions.py @@ -3,11 +3,12 @@ from src.app.schemas.suggestion import NewSuggestion from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student, delete_suggestion, update_suggestion from src.database.models import Suggestion, User -from src.app.schemas.suggestion import SuggestionListResponse +from src.app.schemas.suggestion import SuggestionListResponse, SuggestionResponse from src.app.exceptions.authentication import MissingPermissionsException -def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, user: User, student_id: int) -> None: - create_suggestion(db, user.user_id, student_id, new_suggestion.suggestion, new_suggestion.argumentation) +def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, user: User, student_id: int) -> SuggestionResponse: + suggestion_orm = create_suggestion(db, user.user_id, student_id, new_suggestion.suggestion, new_suggestion.argumentation) + return SuggestionResponse(suggestion=suggestion_orm) def all_suggestions_of_student(db: Session, student_id: int) -> SuggestionListResponse: suggestions_orm = get_suggestions_of_student(db, student_id) diff --git a/backend/src/app/routers/editions/students/suggestions/suggestions.py b/backend/src/app/routers/editions/students/suggestions/suggestions.py index 36991f37c..47cb7ae31 100644 --- a/backend/src/app/routers/editions/students/suggestions/suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/suggestions.py @@ -7,18 +7,18 @@ 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, remove_suggestion, change_suggestion -from src.app.schemas.suggestion import NewSuggestion, SuggestionListResponse +from src.app.schemas.suggestion import NewSuggestion, SuggestionListResponse, SuggestionResponse students_suggestions_router = APIRouter(prefix="/suggestions", tags=[Tags.STUDENTS]) -@students_suggestions_router.post("/", status_code=status.HTTP_201_CREATED) +@students_suggestions_router.post("/", status_code=status.HTTP_201_CREATED, response_model=SuggestionResponse) async def create_suggestion(new_suggestion: NewSuggestion, student: Student = Depends(get_student), db: Session = Depends(get_session), user: User = Depends(require_authorization)): """ Make a suggestion about a student. """ - make_new_suggestion(db, new_suggestion, user, student.student_id) + return 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_authorization), suggestion: Suggestion = Depends(get_suggestion)): diff --git a/backend/src/app/schemas/suggestion.py b/backend/src/app/schemas/suggestion.py index 58f259640..32a75b3f2 100644 --- a/backend/src/app/schemas/suggestion.py +++ b/backend/src/app/schemas/suggestion.py @@ -6,7 +6,7 @@ class NewSuggestion(CamelCaseModel): suggestion: DecisionEnum argumentation: str -class User(CamelCaseModel): +class User(CamelCaseModel): #TODO: delete this when user is on develop and use that one """ Model to represent a Coach Sent as a response to API /GET requests @@ -36,4 +36,10 @@ class SuggestionListResponse(CamelCaseModel): """ A list of suggestions models """ - suggestions: list[Suggestion] \ No newline at end of file + suggestions: list[Suggestion] + +class SuggestionResponse(CamelCaseModel): + """ + the suggestion that is created + """ + suggestion: Suggestion \ No newline at end of file diff --git a/backend/src/database/crud/suggestions.py b/backend/src/database/crud/suggestions.py index d446ce323..87e37f6eb 100644 --- a/backend/src/database/crud/suggestions.py +++ b/backend/src/database/crud/suggestions.py @@ -3,10 +3,11 @@ from src.database.models import Suggestion, Student, User from src.database.enums import DecisionEnum -def create_suggestion(db: Session, user_id: int, student_id: int, decision: DecisionEnum, argumentation: str) -> None: +def create_suggestion(db: Session, user_id: int, student_id: int, decision: DecisionEnum, argumentation: str) -> Suggestion: suggestion: Suggestion = Suggestion(student_id=student_id, coach_id=user_id,suggestion=decision,argumentation=argumentation) db.add(suggestion) db.commit() + return suggestion def get_suggestions_of_student(db: Session, student_id: int) -> list[Suggestion]: return db.query(Suggestion).where(Suggestion.student_id == student_id).all() diff --git a/backend/tests/test_database/test_crud/test_suggestions.py b/backend/tests/test_database/test_crud/test_suggestions.py index 48688bb3d..d7c6bde08 100644 --- a/backend/tests/test_database/test_crud/test_suggestions.py +++ b/backend/tests/test_database/test_crud/test_suggestions.py @@ -15,10 +15,12 @@ def test_create_suggestion_yes(database_session: Session): user: User = database_session.query(User).where(User.name == "coach1").first() student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() - create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") + new_suggestion = create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + assert new_suggestion == suggestion + assert suggestion.coach == user assert suggestion.student == student assert suggestion.suggestion == DecisionEnum.YES @@ -30,10 +32,12 @@ def test_create_suggestion_no(database_session: Session): user: User = database_session.query(User).where(User.name == "coach1").first() student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() - create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.NO, "This is a not good student") + new_suggestion = create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.NO, "This is a not good student") suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + assert new_suggestion == suggestion + assert suggestion.coach == user assert suggestion.student == student assert suggestion.suggestion == DecisionEnum.NO @@ -45,10 +49,12 @@ def test_create_suggestion_maybe(database_session: Session): user: User = database_session.query(User).where(User.name == "coach1").first() student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() - create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") + new_suggestion = create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + assert new_suggestion == suggestion + assert suggestion.coach == user assert suggestion.student == student assert suggestion.suggestion == DecisionEnum.MAYBE 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 d4bda1510..24e95d645 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 @@ -18,9 +18,14 @@ def test_new_suggestion(database_session: Session, test_client: TestClient): } d = test_client.post("/login/token", data=form).json()["accessToken"] auth = "Bearer "+d - assert test_client.post("/editions/1/students/29/suggestions/", headers={"Authorization": auth}, json={"suggestion":1, "argumentation":"test"}).status_code == status.HTTP_201_CREATED + resp = test_client.post("/editions/1/students/29/suggestions/", headers={"Authorization": auth}, json={"suggestion":1, "argumentation":"test"}) + assert resp.status_code == status.HTTP_201_CREATED suggestions: list[Suggestion] = database_session.query(Suggestion).where(Suggestion.student_id==29).all() - assert len(suggestions) > 0 + assert len(suggestions) == 1 + print(resp.json()) + assert resp.json()["suggestion"]["coach"]["email"] == suggestions[0].coach.email + assert DecisionEnum(resp.json()["suggestion"]["suggestion"]) == suggestions[0].suggestion + assert resp.json()["suggestion"]["argumentation"] == suggestions[0].argumentation def test_new_suggestion_not_authorized(database_session: Session, test_client: TestClient): """Tests when not authorized you can't add a new suggestion""" From a92eb52953ee36400895d18eb7f4b4ef3c6d1cd3 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Sun, 20 Mar 2022 11:09:46 +0100 Subject: [PATCH 065/536] got rid of some pylint errors in src --- backend/src/app/logic/suggestions.py | 20 ++++++++++++++----- .../app/routers/editions/students/students.py | 7 ++++--- .../students/suggestions/suggestions.py | 6 +++--- backend/src/app/schemas/suggestion.py | 2 +- backend/src/database/crud/suggestions.py | 11 ++++++++-- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py index 6a947b420..da4b7043e 100644 --- a/backend/src/app/logic/suggestions.py +++ b/backend/src/app/logic/suggestions.py @@ -7,25 +7,35 @@ from src.app.exceptions.authentication import MissingPermissionsException def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, user: User, student_id: int) -> SuggestionResponse: + """"Make a new suggestion""" suggestion_orm = create_suggestion(db, user.user_id, student_id, new_suggestion.suggestion, new_suggestion.argumentation) return SuggestionResponse(suggestion=suggestion_orm) def all_suggestions_of_student(db: Session, student_id: int) -> SuggestionListResponse: + """Get all suggestions of a student""" suggestions_orm = get_suggestions_of_student(db, student_id) return SuggestionListResponse(suggestions=suggestions_orm) def remove_suggestion(db: Session, suggestion: Suggestion, user: User) -> None: - if(user.admin): + """ + Delete a suggestion + Admins can delete all suggestions, coaches only their own suggestions + """ + if user.admin: delete_suggestion(db, suggestion) - elif(suggestion.coach == user): + elif suggestion.coach == user: delete_suggestion(db, suggestion) else: raise MissingPermissionsException def change_suggestion(db: Session, new_suggestion: NewSuggestion, suggestion: Suggestion, user: User) -> None: - if(user.admin): + """ + Update a suggestion + Admins can update all suggestions, coaches only their own suggestions + """ + if user.admin: update_suggestion(db,suggestion,new_suggestion.suggestion, new_suggestion.argumentation) - elif(suggestion.coach == user): + elif suggestion.coach == user: update_suggestion(db,suggestion,new_suggestion.suggestion, new_suggestion.argumentation) else: - raise MissingPermissionsException \ No newline at end of file + raise MissingPermissionsException diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index c374c973a..77d71cf0b 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -2,10 +2,11 @@ from sqlalchemy.orm import Session from src.app.routers.tags import Tags -from .suggestions import students_suggestions_router -from src.app.utils.dependencies import get_edition, require_authorization +from src.app.utils.dependencies import get_edition from src.database.database import get_session -from src.database.models import Edition, User +from src.database.models import Edition + +from .suggestions import students_suggestions_router students_router = APIRouter(prefix="/students", tags=[Tags.STUDENTS]) students_router.include_router(students_suggestions_router, prefix="/{student_id}") diff --git a/backend/src/app/routers/editions/students/suggestions/suggestions.py b/backend/src/app/routers/editions/students/suggestions/suggestions.py index 47cb7ae31..1283f663a 100644 --- a/backend/src/app/routers/editions/students/suggestions/suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/suggestions.py @@ -27,15 +27,15 @@ async def delete_suggestion(db: Session = Depends(get_session), user: User = Dep """ remove_suggestion(db,suggestion,user) -@students_suggestions_router.put("/{suggestion_id}", status_code=status.HTTP_204_NO_CONTENT) -async def edit_suggestion(new_suggestion: NewSuggestion, student: Student = Depends(get_student), db: Session = Depends(get_session), user: User = Depends(require_authorization), suggestion: Suggestion = Depends(get_suggestion)): +@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_authorization), suggestion: Suggestion = Depends(get_suggestion)): """ Edit a suggestion you made about a student. """ change_suggestion(db,new_suggestion,suggestion,user) @students_suggestions_router.get("/", dependencies=[Depends(require_authorization)], status_code=status.HTTP_200_OK, response_model=SuggestionListResponse) -async def get_suggestion(student: Student = Depends(get_student), db: Session = Depends(get_session)): +async def get_suggestions(student: Student = Depends(get_student), db: Session = Depends(get_session)): """ Get all suggestions of a student. """ diff --git a/backend/src/app/schemas/suggestion.py b/backend/src/app/schemas/suggestion.py index 32a75b3f2..b8bed700c 100644 --- a/backend/src/app/schemas/suggestion.py +++ b/backend/src/app/schemas/suggestion.py @@ -42,4 +42,4 @@ class SuggestionResponse(CamelCaseModel): """ the suggestion that is created """ - suggestion: Suggestion \ No newline at end of file + suggestion: Suggestion diff --git a/backend/src/database/crud/suggestions.py b/backend/src/database/crud/suggestions.py index 87e37f6eb..286092bb2 100644 --- a/backend/src/database/crud/suggestions.py +++ b/backend/src/database/crud/suggestions.py @@ -1,24 +1,31 @@ from sqlalchemy.orm import Session -from src.database.models import Suggestion, Student, User +from src.database.models import Suggestion from src.database.enums import DecisionEnum def create_suggestion(db: Session, user_id: int, student_id: int, 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() return suggestion def get_suggestions_of_student(db: Session, student_id: int) -> list[Suggestion]: + """Give all suggestions of a student""" return db.query(Suggestion).where(Suggestion.student_id == student_id).all() def get_suggestion_by_id(db: Session, suggestion_id:int) -> Suggestion: + """Give a suggestion based on the ID""" return db.query(Suggestion).where(Suggestion.suggestion_id == suggestion_id).one() def delete_suggestion(db: Session, suggestion: Suggestion) -> None: + """Delete a suggestion from the database""" db.delete(suggestion) def update_suggestion(db: Session, suggestion: Suggestion, decision: DecisionEnum, argumentation: str) -> None: + """Update a suggestion""" suggestion.suggestion = decision suggestion.argumentation = argumentation - db.commit() \ No newline at end of file + db.commit() From 9959bba31d0402f6db325f6b06d1be28de58af58 Mon Sep 17 00:00:00 2001 From: beguille Date: Sun, 20 Mar 2022 11:31:01 +0100 Subject: [PATCH 066/536] fixed small typing issue, added docstrings to functions and classes --- backend/src/app/logic/projects.py | 11 +++++--- backend/src/app/logic/projects_students.py | 3 +++ backend/src/app/schemas/projects.py | 13 +++++++++- backend/src/database/crud/projects.py | 26 ++++++++++++++----- .../src/database/crud/projects_students.py | 9 ++++--- 5 files changed, 49 insertions(+), 13 deletions(-) diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index d98373d7d..f2ec4366b 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -7,24 +7,29 @@ def logic_get_project_list(db: Session, edition: Edition) -> ProjectList: + """Returns a list of all projects from a certain edition""" db_all_projects = db_get_all_projects(db, edition) return ProjectList(projects=db_all_projects) -def logic_create_project(db: Session, edition: Edition, name: str, number_of_students: int, skills: [int], - partners: [str], coaches: [int]) -> ProjectId: +def logic_create_project(db: Session, edition: Edition, name: str, number_of_students: int, skills: list[int], + partners: list[str], coaches: list[int]) -> ProjectId: + """Create a new project""" return db_add_project(db, edition, name, number_of_students, skills, partners, coaches) def logic_delete_project(db: Session, project_id: int): + """Delete a project""" db_delete_project(db, project_id) def logic_patch_project(db: Session, project: Project, name: str, number_of_students: int, skills: [int], - partners: [str], coaches: [int]): + partners: list[str], coaches: list[int]): + """Make changes to a project""" db_patch_project(db, project, name, number_of_students, skills, partners, coaches) def logic_get_conflicts(db: Session, edition: Edition) -> ConflictStudentList: + """Returns a list of all students together with the projects they are causing a conflict for""" conflicts = db_get_conflict_students(db, edition) return ConflictStudentList(conflict_students=conflicts) diff --git a/backend/src/app/logic/projects_students.py b/backend/src/app/logic/projects_students.py index 3a9a58b3a..dea58d6c3 100644 --- a/backend/src/app/logic/projects_students.py +++ b/backend/src/app/logic/projects_students.py @@ -6,12 +6,15 @@ def logic_remove_student_project(db: Session, project: Project, student_id: int): + """Remove a student from a project""" db_remove_student_project(db, project, student_id) def logic_add_student_project(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): + """Add a student to a project""" db_add_student_project(db, project, student_id, skill_id, drafter_id) def logic_change_project_role(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): + """Change the role of the student in the project""" db_change_project_role(db, project, student_id, skill_id, drafter_id) diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index 905a3aa65..9a3459f29 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -5,6 +5,7 @@ class User(CamelCaseModel): + """Represents a User from the database""" user_id: int name: str email: str @@ -14,6 +15,7 @@ class Config: class Skill(CamelCaseModel): + """Represents a Skill from the database""" skill_id: int name: str description: str @@ -23,6 +25,7 @@ class Config: class Partner(CamelCaseModel): + """Represents a Partner from the database""" partner_id: int name: str @@ -31,6 +34,7 @@ class Config: class ProjectRole(CamelCaseModel): + """Represents a ProjectRole from the database""" student_id: int project_id: int skill_id: int @@ -43,6 +47,7 @@ class Config: 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 @@ -58,6 +63,7 @@ class Config: class Student(CamelCaseModel): + """Represents a Student from the database to use in ConflictStudent""" student_id: int first_name: str last_name: str @@ -74,23 +80,28 @@ class Config: class ProjectList(CamelCaseModel): + """A list of projects""" projects: list[Project] class ConflictStudent(CamelCaseModel): + """A student together with the projects they are causing a conflict for""" student: Student projects: list[Project] class ConflictStudentList(CamelCaseModel): + """A list of ConflictStudents""" conflict_students: list[ConflictStudent] class ProjectId(CamelCaseModel): + """Used for returning the id of a project after creating it""" project_id: 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] @@ -100,6 +111,6 @@ class InputProject(BaseModel): # TODO: change drafter_id to current user with authentication class InputStudentRole(BaseModel): + """Used for creating/patching a student role (temporary until authentication is implemented)""" skill_id: int drafter_id: int - diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index f8afaa127..f6654330d 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -6,11 +6,16 @@ def db_get_all_projects(db: Session, edition: Edition) -> list[Project]: + """Query all projects from a certain edition from the database""" return db.query(Project).where(Project.edition == edition).all() def db_add_project(db: Session, edition: Edition, name: str, number_of_students: int, skills: list[int], partners: list[str], coaches: list[int]) -> ProjectId: + """ + 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 skills] coaches_obj = [db.query(User).where(User.user_id) == coach for coach in coaches] partners_obj = [] @@ -30,13 +35,15 @@ def db_add_project(db: Session, edition: Edition, name: str, number_of_students: def db_get_project(db: Session, 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() def db_delete_project(db: Session, project_id: int): + """Delete a specific project from the database""" proj_roles = db.query(ProjectRole).where(ProjectRole.project_id == project_id).all() - for pr in proj_roles: - db.delete(pr) + for proj_role in proj_roles: + db.delete(proj_role) project = db_get_project(db, project_id) db.delete(project) @@ -45,6 +52,10 @@ def db_delete_project(db: Session, project_id: int): def db_patch_project(db: Session, project: Project, name: str, number_of_students: int, skills: list[int], partners: list[str], coaches: list[int]): + """ + Change some fields of a Project in 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 skills] coaches_obj = [db.query(User).where(User.user_id) == coach for coach in coaches] partners_obj = [] @@ -65,7 +76,11 @@ def db_patch_project(db: Session, project: Project, name: str, number_of_student def db_get_conflict_students(db: Session, edition: Edition) -> list[ConflictStudent]: - + """ + 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 + """ students = db.query(Student).where(Student.edition == edition).all() conflict_students = [] projs = [] @@ -76,7 +91,6 @@ def db_get_conflict_students(db: Session, edition: Edition) -> list[ConflictStud proj_id = proj_id[0] proj = db.query(Project).where(Project.project_id == proj_id).one() projs.append(proj) - cp = ConflictStudent(student=student, projects=projs) - conflict_students.append(cp) + conflict_student = ConflictStudent(student=student, projects=projs) + conflict_students.append(conflict_student) return conflict_students - diff --git a/backend/src/database/crud/projects_students.py b/backend/src/database/crud/projects_students.py index 22403ae5b..7830488ed 100644 --- a/backend/src/database/crud/projects_students.py +++ b/backend/src/database/crud/projects_students.py @@ -4,12 +4,14 @@ def db_remove_student_project(db: Session, project: Project, student_id: int): + """Remove a student from a project in the database""" pr = db.query(ProjectRole).where(ProjectRole.student_id == student_id and ProjectRole.project == project).one() db.delete(pr) db.commit() def db_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""" proj_role = ProjectRole(student_id=student_id, project_id=project.project_id, skill_id=skill_id, drafter_id=drafter_id) db.add(proj_role) @@ -17,7 +19,8 @@ def db_add_student_project(db: Session, project: Project, student_id: int, skill def db_change_project_role(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): - pr = db.query(ProjectRole).where(ProjectRole.student_id == student_id and ProjectRole.project == project).one() - pr.drafter_id = drafter_id - pr.skill_id = skill_id + """Change the role of a student in a project and update the drafter""" + proj_role = db.query(ProjectRole).where(ProjectRole.student_id == student_id and ProjectRole.project == project).one() + proj_role.drafter_id = drafter_id + proj_role.skill_id = skill_id db.commit() From cd86638264c630b82899e0a8615d338ebb4c80c8 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Sun, 20 Mar 2022 12:12:23 +0100 Subject: [PATCH 067/536] fixed some pylint errors --- .../test_database/test_crud/test_students.py | 9 +- .../test_crud/test_suggestions.py | 178 ++++++++++++------ .../test_suggestions/test_suggestions.py | 159 ++++++++++------ 3 files changed, 229 insertions(+), 117 deletions(-) diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index 221d92548..f9afc7dcf 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -1,11 +1,12 @@ import pytest from sqlalchemy.orm import Session -from tests.fill_database import fill_database +from sqlalchemy.orm.exc import NoResultFound from src.database.models import Student from src.database.crud.students import get_student_by_id -from sqlalchemy.orm.exc import NoResultFound +from tests.fill_database import fill_database def test_get_student_by_id(database_session: Session): + """Tests if you get the right student""" fill_database(database_session) student: Student = get_student_by_id(database_session, 1) assert student.first_name == "Jos" @@ -13,6 +14,8 @@ def test_get_student_by_id(database_session: Session): assert student.student_id == 1 assert student.email_address == "josvermeulen@mail.com" + def test_no_student(database_session: Session): + """Tests if you get an error for a not existing student""" with pytest.raises(NoResultFound): - get_student_by_id(database_session, 5) \ No newline at end of file + get_student_by_id(database_session, 5) diff --git a/backend/tests/test_database/test_crud/test_suggestions.py b/backend/tests/test_database/test_crud/test_suggestions.py index d7c6bde08..da4fa3ae3 100644 --- a/backend/tests/test_database/test_crud/test_suggestions.py +++ b/backend/tests/test_database/test_crud/test_suggestions.py @@ -1,23 +1,29 @@ import pytest from sqlalchemy.orm import Session from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm.exc import NoResultFound from src.database.models import Suggestion, Student, User from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student, get_suggestion_by_id, delete_suggestion, update_suggestion -from tests.fill_database import fill_database from src.database.enums import DecisionEnum -from sqlalchemy.orm.exc import NoResultFound +from tests.fill_database import fill_database + def test_create_suggestion_yes(database_session: Session): + """Test creat a yes suggestion""" fill_database(database_session) - user: User = database_session.query(User).where(User.name == "coach1").first() - student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() - - new_suggestion = create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") + user: User = database_session.query( + User).where(User.name == "coach1").first() + student: Student = database_session.query(Student).where( + Student.email_address == "marta.marquez@example.com").first() + + new_suggestion = create_suggestion( + database_session, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") - suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + suggestion: Suggestion = database_session.query(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() assert new_suggestion == suggestion @@ -26,15 +32,21 @@ def test_create_suggestion_yes(database_session: Session): assert suggestion.suggestion == DecisionEnum.YES assert suggestion.argumentation == "This is a good student" + def test_create_suggestion_no(database_session: Session): + """Test create a no suggestion""" fill_database(database_session) - user: User = database_session.query(User).where(User.name == "coach1").first() - student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() - - new_suggestion = create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.NO, "This is a not good student") + user: User = database_session.query( + User).where(User.name == "coach1").first() + student: Student = database_session.query(Student).where( + Student.email_address == "marta.marquez@example.com").first() + + new_suggestion = create_suggestion( + database_session, user.user_id, student.student_id, DecisionEnum.NO, "This is a not good student") - suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + suggestion: Suggestion = database_session.query(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() assert new_suggestion == suggestion @@ -43,15 +55,21 @@ def test_create_suggestion_no(database_session: Session): assert suggestion.suggestion == DecisionEnum.NO assert suggestion.argumentation == "This is a not good student" + def test_create_suggestion_maybe(database_session: Session): + """Test create a maybe suggestion""" fill_database(database_session) - user: User = database_session.query(User).where(User.name == "coach1").first() - student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() - - new_suggestion = create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") + user: User = database_session.query( + User).where(User.name == "coach1").first() + student: Student = database_session.query(Student).where( + Student.email_address == "marta.marquez@example.com").first() + + new_suggestion = create_suggestion( + database_session, user.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") - suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + suggestion: Suggestion = database_session.query(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() assert new_suggestion == suggestion @@ -60,54 +78,79 @@ def test_create_suggestion_maybe(database_session: Session): assert suggestion.suggestion == DecisionEnum.MAYBE assert suggestion.argumentation == "Idk if it's good student" + def test_one_coach_two_students(database_session: Session): + """Test that one coach can write multiple suggestions""" fill_database(database_session) - user: User = database_session.query(User).where(User.name == "coach1").one() - student1: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").one() - student2: Student = database_session.query(Student).where(Student.email_address == "sofia.haataja@example.com").one() - - create_suggestion(database_session, user.user_id, student1.student_id, DecisionEnum.YES, "This is a good student") - create_suggestion(database_session, user.user_id, student2.student_id, DecisionEnum.NO, "This is a not good student") + user: User = database_session.query( + User).where(User.name == "coach1").one() + student1: Student = database_session.query(Student).where( + Student.email_address == "marta.marquez@example.com").one() + student2: Student = database_session.query(Student).where( + Student.email_address == "sofia.haataja@example.com").one() + + create_suggestion(database_session, user.user_id, + student1.student_id, DecisionEnum.YES, "This is a good student") + create_suggestion(database_session, user.user_id, student2.student_id, + DecisionEnum.NO, "This is a not good student") - suggestion1: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student1.student_id).one() + suggestion1: Suggestion = database_session.query(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student1.student_id).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_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student2.student_id).one() + suggestion2: Suggestion = database_session.query(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student2.student_id).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_session: Session): + """Test get multiple suggestions about the same student""" fill_database(database_session) - user: User = database_session.query(User).where(User.name == "coach1").first() - student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() - - create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") + user: User = database_session.query( + User).where(User.name == "coach1").first() + student: Student = database_session.query(Student).where( + Student.email_address == "marta.marquez@example.com").first() + + create_suggestion(database_session, user.user_id, student.student_id, + DecisionEnum.MAYBE, "Idk if it's good student") with pytest.raises(IntegrityError): - create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") - + create_suggestion(database_session, user.user_id, + student.student_id, DecisionEnum.YES, "This is a good student") + + def test_get_suggestions_of_student(database_session: Session): + """Test get all suggestions of a student""" fill_database(database_session) - user1: User = database_session.query(User).where(User.name == "coach1").first() - user2: User = database_session.query(User).where(User.name == "coach2").first() - student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() - - create_suggestion(database_session, user1.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") - create_suggestion(database_session, user2.user_id, student.student_id, DecisionEnum.YES, "This is a good student") - suggestions_student = get_suggestions_of_student(database_session, student.student_id) - + user1: User = database_session.query( + User).where(User.name == "coach1").first() + user2: User = database_session.query( + User).where(User.name == "coach2").first() + student: Student = database_session.query(Student).where( + Student.email_address == "marta.marquez@example.com").first() + + create_suggestion(database_session, user1.user_id, student.student_id, + DecisionEnum.MAYBE, "Idk if it's good student") + create_suggestion(database_session, user2.user_id, + student.student_id, DecisionEnum.YES, "This is a good student") + suggestions_student = get_suggestions_of_student( + database_session, student.student_id) + assert len(suggestions_student) == 2 assert suggestions_student[0].student == student assert suggestions_student[1].student == student + def test_get_suggestion_by_id(database_session: Session): + """Test get suggestion by id""" fill_database(database_session) suggestion: Suggestion = get_suggestion_by_id(database_session, 1) assert suggestion.student_id == 1 @@ -115,35 +158,52 @@ def test_get_suggestion_by_id(database_session: Session): assert suggestion.suggestion == DecisionEnum.YES assert suggestion.argumentation == "Good student" + def test_get_suggestion_by_id_non_existing(database_session: Session): + """Test you get an error when you search an id that don't exist""" with pytest.raises(NoResultFound): - suggestion: Suggestion = get_suggestion_by_id(database_session, 1) + get_suggestion_by_id(database_session, 1) + def test_delete_suggestion(database_session: Session): + """Test delete suggestion""" fill_database(database_session) - user: User = database_session.query(User).where(User.name == "coach1").first() - student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() - - create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") - suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() - + user: User = database_session.query( + User).where(User.name == "coach1").first() + student: Student = database_session.query(Student).where( + Student.email_address == "marta.marquez@example.com").first() + + create_suggestion(database_session, user.user_id, + student.student_id, DecisionEnum.YES, "This is a good student") + suggestion: Suggestion = database_session.query(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + delete_suggestion(database_session, suggestion) - - suggestions: list[Suggestion] = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).all() + + suggestions: list[Suggestion] = database_session.query(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student.student_id).all() assert len(suggestions) == 0 + def test_update_suggestion(database_session: Session): + """Test update suggestion""" fill_database(database_session) - user: User = database_session.query(User).where(User.name == "coach1").first() - student: Student = database_session.query(Student).where(Student.email_address == "marta.marquez@example.com").first() - - create_suggestion(database_session, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") - suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() - - update_suggestion(database_session, suggestion, DecisionEnum.NO, "Not that good student") - - new_suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() - assert suggestion.suggestion == DecisionEnum.NO - assert suggestion.argumentation == "Not that good student" \ No newline at end of file + user: User = database_session.query( + User).where(User.name == "coach1").first() + student: Student = database_session.query(Student).where( + Student.email_address == "marta.marquez@example.com").first() + + create_suggestion(database_session, user.user_id, + student.student_id, DecisionEnum.YES, "This is a good student") + suggestion: Suggestion = database_session.query(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + + update_suggestion(database_session, suggestion, + DecisionEnum.NO, "Not that good student") + + new_suggestion: Suggestion = database_session.query(Suggestion).where( + Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() + assert new_suggestion.suggestion == DecisionEnum.NO + assert new_suggestion.argumentation == "Not that good student" 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 24e95d645..95eba8f58 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 @@ -2,8 +2,8 @@ from starlette import status from starlette.testclient import TestClient from src.database.enums import DecisionEnum +from src.database.models import Suggestion from tests.fill_database import fill_database -from src.database.models import Suggestion, Student, User def test_new_suggestion(database_session: Session, test_client: TestClient): @@ -16,30 +16,39 @@ def test_new_suggestion(database_session: Session, test_client: TestClient): "username": email, "password": password } - d = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer "+d - resp = test_client.post("/editions/1/students/29/suggestions/", headers={"Authorization": auth}, json={"suggestion":1, "argumentation":"test"}) + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + resp = test_client.post("/editions/1/students/29/suggestions/", headers={ + "Authorization": auth}, json={"suggestion": 1, "argumentation": "test"}) assert resp.status_code == status.HTTP_201_CREATED - suggestions: list[Suggestion] = database_session.query(Suggestion).where(Suggestion.student_id==29).all() + suggestions: list[Suggestion] = database_session.query( + Suggestion).where(Suggestion.student_id == 29).all() assert len(suggestions) == 1 print(resp.json()) - assert resp.json()["suggestion"]["coach"]["email"] == suggestions[0].coach.email - assert DecisionEnum(resp.json()["suggestion"]["suggestion"]) == suggestions[0].suggestion - assert resp.json()["suggestion"]["argumentation"] == suggestions[0].argumentation + assert resp.json()[ + "suggestion"]["coach"]["email"] == suggestions[0].coach.email + assert DecisionEnum(resp.json()["suggestion"] + ["suggestion"]) == suggestions[0].suggestion + assert resp.json()[ + "suggestion"]["argumentation"] == suggestions[0].argumentation + def test_new_suggestion_not_authorized(database_session: Session, test_client: TestClient): """Tests when not authorized you can't add a new suggestion""" fill_database(database_session) - assert test_client.post("/editions/1/students/29/suggestions/", json={"suggestion":1, "argumentation":"test"}).status_code == status.HTTP_401_UNAUTHORIZED - suggestions: list[Suggestion] = database_session.query(Suggestion).where(Suggestion.student_id==29).all() + assert test_client.post("/editions/1/students/29/suggestions/", json={ + "suggestion": 1, "argumentation": "test"}).status_code == status.HTTP_401_UNAUTHORIZED + suggestions: list[Suggestion] = database_session.query( + Suggestion).where(Suggestion.student_id == 29).all() assert len(suggestions) == 0 def test_get_suggestions_of_student_not_authorized(database_session: Session, test_client: TestClient): """Tests if you don't have the right access, you get the right HTTP code""" - assert test_client.get("/editions/1/students/29/suggestions/", headers={"Authorization": "auth"}, json={"suggestion":1, "argumentation":"Ja"}).status_code == status.HTTP_401_UNAUTHORIZED + assert test_client.get("/editions/1/students/29/suggestions/", headers={"Authorization": "auth"}, json={ + "suggestion": 1, "argumentation": "Ja"}).status_code == status.HTTP_401_UNAUTHORIZED def test_get_suggestions_of_ghost(database_session: Session, test_client: TestClient): @@ -52,11 +61,12 @@ def test_get_suggestions_of_ghost(database_session: Session, test_client: TestCl "username": email, "password": password } - d = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer "+d - res = test_client.get("/editions/1/students/9000/suggestions/", headers={"Authorization": auth}) + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + res = test_client.get( + "/editions/1/students/9000/suggestions/", headers={"Authorization": auth}) assert res.status_code == status.HTTP_404_NOT_FOUND - + def test_get_suggestions_of_student(database_session: Session, test_client: TestClient): """Tests to get the suggestions of a student""" @@ -66,17 +76,20 @@ def test_get_suggestions_of_student(database_session: Session, test_client: Test "username": "coach1@noutlook.be", "password": "wachtwoord" } - d = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer "+d - assert test_client.post("/editions/1/students/29/suggestions/", headers={"Authorization": auth}, json={"suggestion":1, "argumentation":"Ja"}).status_code == status.HTTP_201_CREATED + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + assert test_client.post("/editions/1/students/29/suggestions/", headers={"Authorization": auth}, json={ + "suggestion": 1, "argumentation": "Ja"}).status_code == status.HTTP_201_CREATED form = { "username": "admin@ngmail.com", "password": "wachtwoord" } - d = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer "+d - assert test_client.post("/editions/1/students/29/suggestions/", headers={"Authorization": auth}, json={"suggestion":3, "argumentation":"Neen"}).status_code == status.HTTP_201_CREATED - res = test_client.get("/editions/1/students/29/suggestions/", headers={"Authorization": auth}) + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + assert test_client.post("/editions/1/students/29/suggestions/", headers={"Authorization": auth}, json={ + "suggestion": 3, "argumentation": "Neen"}).status_code == status.HTTP_201_CREATED + res = test_client.get( + "/editions/1/students/29/suggestions/", headers={"Authorization": auth}) assert res.status_code == status.HTTP_200_OK res_json = res.json() assert len(res_json["suggestions"]) == 2 @@ -87,105 +100,141 @@ def test_get_suggestions_of_student(database_session: Session, test_client: Test assert res_json["suggestions"][1]["suggestion"] == 3 assert res_json["suggestions"][1]["argumentation"] == "Neen" + def test_delete_ghost_suggestion(database_session: Session, test_client: TestClient): + """Tests that you get the correct status code when you delete a not existing suggestion""" fill_database(database_session) form = { "username": "admin@ngmail.com", "password": "wachtwoord" } - d = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer "+d - assert test_client.delete("/editions/1/students/1/suggestions/8000", headers={"Authorization": auth}).status_code == status.HTTP_404_NOT_FOUND + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + assert test_client.delete("/editions/1/students/1/suggestions/8000", headers={ + "Authorization": auth}).status_code == status.HTTP_404_NOT_FOUND + def test_delete_not_autorized(database_session: Session, test_client: TestClient): + """Tests that you have to be loged in for deleating a suggestion""" fill_database(database_session) - assert test_client.delete("/editions/1/students/1/suggestions/8000", headers={"Authorization": "auth"}).status_code == status.HTTP_401_UNAUTHORIZED + assert test_client.delete("/editions/1/students/1/suggestions/8000", headers={ + "Authorization": "auth"}).status_code == status.HTTP_401_UNAUTHORIZED + def test_delete_suggestion_admin(database_session: Session, test_client: TestClient): + """Test that an admin can update suggestions""" fill_database(database_session) form = { "username": "admin@ngmail.com", "password": "wachtwoord" } - d = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer "+d - assert test_client.delete("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}).status_code == status.HTTP_204_NO_CONTENT - suggestions: Suggestion = database_session.query(Suggestion).where(Suggestion.suggestion_id==1).all() + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + assert test_client.delete("/editions/1/students/1/suggestions/1", headers={ + "Authorization": auth}).status_code == status.HTTP_204_NO_CONTENT + suggestions: Suggestion = database_session.query( + Suggestion).where(Suggestion.suggestion_id == 1).all() assert len(suggestions) == 0 + def test_delete_suggestion_coach_their_review(database_session: Session, test_client: TestClient): + """Tests that a coach can delete their own suggestion""" fill_database(database_session) form = { "username": "coach1@noutlook.be", "password": "wachtwoord" } - d = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer "+d - assert test_client.delete("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}).status_code == status.HTTP_204_NO_CONTENT - suggestions: Suggestion = database_session.query(Suggestion).where(Suggestion.suggestion_id==1).all() + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + assert test_client.delete("/editions/1/students/1/suggestions/1", headers={ + "Authorization": auth}).status_code == status.HTTP_204_NO_CONTENT + suggestions: Suggestion = database_session.query( + Suggestion).where(Suggestion.suggestion_id == 1).all() assert len(suggestions) == 0 + def test_delete_suggestion_coach_other_review(database_session: Session, test_client: TestClient): + """Tests that a coach can't delete other coaches their suggestions""" fill_database(database_session) form = { "username": "coach2@noutlook.be", "password": "wachtwoord" } - d = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer "+d - assert test_client.delete("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}).status_code == status.HTTP_403_FORBIDDEN - suggestions: Suggestion = database_session.query(Suggestion).where(Suggestion.suggestion_id==1).all() + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + assert test_client.delete("/editions/1/students/1/suggestions/1", headers={ + "Authorization": auth}).status_code == status.HTTP_403_FORBIDDEN + suggestions: Suggestion = database_session.query( + Suggestion).where(Suggestion.suggestion_id == 1).all() assert len(suggestions) == 1 + def test_update_ghost_suggestion(database_session: Session, test_client: TestClient): + """Tests a suggestion that don't exist """ fill_database(database_session) form = { "username": "admin@ngmail.com", "password": "wachtwoord" } - d = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer "+d - assert test_client.put("/editions/1/students/1/suggestions/8000", headers={"Authorization": auth}, json={"suggestion":1, "argumentation":"test"}).status_code == status.HTTP_404_NOT_FOUND + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + assert test_client.put("/editions/1/students/1/suggestions/8000", headers={"Authorization": auth}, json={ + "suggestion": 1, "argumentation": "test"}).status_code == status.HTTP_404_NOT_FOUND + def test_update_not_autorized(database_session: Session, test_client: TestClient): + """Tests update when not autorized""" fill_database(database_session) - assert test_client.put("/editions/1/students/1/suggestions/8000", headers={"Authorization": "auth"}, json={"suggestion":1, "argumentation":"test"}).status_code == status.HTTP_401_UNAUTHORIZED + assert test_client.put("/editions/1/students/1/suggestions/8000", headers={"Authorization": "auth"}, json={ + "suggestion": 1, "argumentation": "test"}).status_code == status.HTTP_401_UNAUTHORIZED + def test_update_suggestion_admin(database_session: Session, test_client: TestClient): + """Test that an admin can update suggestions""" fill_database(database_session) form = { "username": "admin@ngmail.com", "password": "wachtwoord" } - d = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer "+d - assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}, json={"suggestion":3, "argumentation":"test"}).status_code == status.HTTP_204_NO_CONTENT - suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.suggestion_id==1).one() + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}, json={ + "suggestion": 3, "argumentation": "test"}).status_code == status.HTTP_204_NO_CONTENT + suggestion: Suggestion = database_session.query( + Suggestion).where(Suggestion.suggestion_id == 1).one() assert suggestion.suggestion == DecisionEnum.NO assert suggestion.argumentation == "test" + def test_update_suggestion_coach_their_review(database_session: Session, test_client: TestClient): + """Tests that a coach can update their own suggestion""" fill_database(database_session) form = { "username": "coach1@noutlook.be", "password": "wachtwoord" } - d = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer "+d - assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}, json={"suggestion":3, "argumentation":"test"}).status_code == status.HTTP_204_NO_CONTENT - suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.suggestion_id==1).one() + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}, json={ + "suggestion": 3, "argumentation": "test"}).status_code == status.HTTP_204_NO_CONTENT + suggestion: Suggestion = database_session.query( + Suggestion).where(Suggestion.suggestion_id == 1).one() assert suggestion.suggestion == DecisionEnum.NO assert suggestion.argumentation == "test" + def test_update_suggestion_coach_other_review(database_session: Session, test_client: TestClient): + """Tests that a coach can't update other coaches their suggestions""" fill_database(database_session) form = { "username": "coach2@noutlook.be", "password": "wachtwoord" } - d = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer "+d - assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}, json={"suggestion":3, "argumentation":"test"}).status_code == status.HTTP_403_FORBIDDEN - suggestion: Suggestion = database_session.query(Suggestion).where(Suggestion.suggestion_id==1).one() + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}, json={ + "suggestion": 3, "argumentation": "test"}).status_code == status.HTTP_403_FORBIDDEN + suggestion: Suggestion = database_session.query( + Suggestion).where(Suggestion.suggestion_id == 1).one() assert suggestion.suggestion != DecisionEnum.NO - assert suggestion.argumentation != "test" \ No newline at end of file + assert suggestion.argumentation != "test" From a9e77e7aacfea2ec33f44958c2dc9711e11058b3 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Mon, 21 Mar 2022 12:31:44 +0100 Subject: [PATCH 068/536] delete fill_database, and rewrite tests --- backend/tests/fill_database.py | 172 ------------------ .../test_database/test_crud/test_students.py | 48 ++++- .../test_crud/test_suggestions.py | 61 ++++++- .../test_suggestions/test_suggestions.py | 80 +++++++- 4 files changed, 174 insertions(+), 187 deletions(-) delete mode 100644 backend/tests/fill_database.py diff --git a/backend/tests/fill_database.py b/backend/tests/fill_database.py deleted file mode 100644 index 181638b8b..000000000 --- a/backend/tests/fill_database.py +++ /dev/null @@ -1,172 +0,0 @@ -from sqlalchemy.orm import Session - -from src.database.models import * -from src.database.enums import * -from src.app.logic.security import get_password_hash -from src.database.enums import DecisionEnum -from datetime import date - -def fill_database(db: Session): - """A function to fill the database with fake data that can easly be used when testing""" - # Editions - edition: Edition = Edition(year = 2022) - db.add(edition) - db.commit() - - # Users - admin: User = User(name="admin", email="admin@ngmail.com", admin=True) - coach1: User = User(name="coach1", email="coach1@noutlook.be") - coach2: User = User(name="coach2", email="coach2@noutlook.be") - request: User = User(name="request", email="request@ngmail.com") - db.add(admin) - db.add(coach1) - db.add(coach2) - db.add(request) - db.commit() - - # AuthEmail - pw_hash = get_password_hash("wachtwoord") - auth_email_admin: AuthEmail = AuthEmail(user=admin, pw_hash=pw_hash) - auth_email_coach1: AuthEmail = AuthEmail(user=coach1, pw_hash=pw_hash) - auth_email_coach2: AuthEmail = AuthEmail(user=coach2, pw_hash=pw_hash) - auth_email_request: AuthEmail = AuthEmail(user=request, pw_hash=pw_hash) - db.add(auth_email_admin) - db.add(auth_email_coach1) - db.add(auth_email_coach2) - db.add(auth_email_request) - 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") - db.add(skill1) - db.add(skill2) - db.add(skill3) - db.add(skill4) - db.add(skill5) - db.add(skill6) - db.commit() - - #Student - 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, skill6]) - 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, skill5]) - 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, skill4, skill5]) - student04: Student = Student(first_name="Délano", last_name="Van Lienden", preferred_name="Délano",email_address="delano.vanlienden@example.com", phone_number="(128)-049-9143", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - student05: Student = Student(first_name="Einar", last_name="Rossebø", preferred_name="Einar",email_address="einar.rossebo@example.com", phone_number="61491822", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[skill2, skill4, skill5]) - student06: Student = Student(first_name="Dave", last_name="Johnston", preferred_name="Dave",email_address="dave.johnston@example.com", phone_number="031-156-2869", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[skill2, skill4, skill5]) - student07: Student = Student(first_name="Fernando", last_name="Stone", preferred_name="Fernando",email_address="fernando.stone@example.com", phone_number="(441)-156-4776", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - student08: Student = Student(first_name="Isabelle", last_name="Singh", preferred_name="Isabelle",email_address="isabelle.singh@example.com", phone_number="(338)-531-9957", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[skill2, skill4, skill5]) - student09: Student = Student(first_name="Blake", last_name="Martin", preferred_name="Blake",email_address="blake.martin@example.com", phone_number="404-060-5843", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - student10: 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=[skill2]) - student11: Student = Student(first_name="Mehmet", last_name="Balcı", preferred_name="Mehmet",email_address="mehmet.balci@example.com", phone_number="(496)-221-8222", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - student12: Student = Student(first_name="Óscar", last_name="das Neves", preferred_name="Óscar",email_address="oscar.dasneves@example.com", phone_number="(47) 6646-0730", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[skill4]) - student13: Student = Student(first_name="Melike", last_name="Süleymanoğlu", preferred_name="Melike",email_address="melike.suleymanoglu@example.com", phone_number="(274)-545-3055", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[skill2, skill4, skill5]) - student14: Student = Student(first_name="Magnus", last_name="Schanke", preferred_name="Magnus",email_address="magnus.schanke@example.com", phone_number="63507430", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[skill2, skill4, skill5]) - student15: Student = Student(first_name="Tara", last_name="Howell", preferred_name="Tara",email_address="tara.howell@example.com", phone_number="07-9111-0958", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - student16: Student = Student(first_name="Hanni", last_name="Ewers", preferred_name="Hanni",email_address="hanni.ewers@example.com", phone_number="0241-5176890", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill1, skill6, skill5]) - student17: Student = Student(first_name="آیناز", last_name="کریمی", preferred_name="آیناز",email_address="aynz.khrymy@example.com", phone_number="009-26345191", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[skill2, skill4, skill5]) - student18: Student = Student(first_name="Vicente", last_name="Garrido", preferred_name="Vicente",email_address="vicente.garrido@example.com", phone_number="987-381-670", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - student19: Student = Student(first_name="Elmer", last_name="Morris", preferred_name="Elmer",email_address="elmer.morris@example.com", phone_number="(611)-832-8108", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - student20: Student = Student(first_name="Alexis", last_name="Roy", preferred_name="Alexis",email_address="alexis.roy@example.com", phone_number="566-546-7642", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - student21: Student = Student(first_name="Lillie", last_name="Kelly", preferred_name="Lillie",email_address="lillie.kelly@example.com", phone_number="(983)-560-1392", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - student22: Student = Student(first_name="Karola", last_name="Andersen", preferred_name="Karola",email_address="karola.andersen@example.com", phone_number="0393-3219328", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - student23: Student = Student(first_name="Elvine", last_name="Andvik", preferred_name="Elvine",email_address="elvine.andvik@example.com", phone_number="30454610", alumni=True, wants_to_be_student_coach=True, edition=edition, skills=[skill2, skill4, skill5]) - student24: Student = Student(first_name="Chris", last_name="Kelly", preferred_name="Chris",email_address="chris.kelly@example.com", phone_number="061-399-0053", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill4]) - student25: Student = Student(first_name="Aada", last_name="Pollari", preferred_name="Aada",email_address="aada.pollari@example.com", phone_number="02-908-609", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - student26: Student = Student(first_name="Sofia", last_name="Haataja", preferred_name="Sofia",email_address="sofia.haataja@example.com", phone_number="06-373-889", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - student27: Student = Student(first_name="Charlene", last_name="Gregory", preferred_name="Charlene",email_address="charlene.gregory@example.com", phone_number="(991)-378-7095", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - student28: Student = Student(first_name="Danielle", last_name="Chavez", preferred_name="Danielle",email_address="danielle.chavez@example.com", phone_number="01435 91142", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - student29: Student = Student(first_name="Nikolaj", last_name="Poulsen", preferred_name="Nikolaj",email_address="nikolaj.poulsen@example.com", phone_number="20525141", alumni=False, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - student30: Student = Student(first_name="Marta", last_name="Marquez", preferred_name="Marta",email_address="marta.marquez@example.com", phone_number="967-895-285", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - student31: Student = Student(first_name="Gül", last_name="Barbarosoğlu", preferred_name="Gül",email_address="gul.barbarosoglu@example.com", phone_number="(008)-316-3264", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - - db.add(student01) - db.add(student02) - db.add(student03) - db.add(student04) - db.add(student05) - db.add(student06) - db.add(student07) - db.add(student08) - db.add(student09) - db.add(student10) - db.add(student11) - db.add(student12) - db.add(student13) - db.add(student14) - db.add(student15) - db.add(student16) - db.add(student17) - db.add(student18) - db.add(student19) - db.add(student20) - db.add(student21) - db.add(student22) - db.add(student23) - db.add(student24) - db.add(student25) - db.add(student26) - db.add(student27) - db.add(student28) - db.add(student29) - db.add(student30) - db.add(student31) - db.commit() - - # CoachRequest - coach_request: CoachRequest = CoachRequest(edition=edition, user=request) - db.add(coach_request) - db.commit() - - #DecisionEmail - decision_email1 : DecisionEmail = DecisionEmail(decision=DecisionEnum.NO, student=student29, date=date.today()) - decision_email2 : DecisionEmail = DecisionEmail(decision=DecisionEnum.YES, student=student09, date=date.today()) - decision_email3 : DecisionEmail = DecisionEmail(decision=DecisionEnum.YES, student=student10, date=date.today()) - decision_email4 : DecisionEmail = DecisionEmail(decision=DecisionEnum.YES, student=student11, date=date.today()) - decision_email5 : DecisionEmail = DecisionEmail(decision=DecisionEnum.YES, student=student12, date=date.today()) - decision_email6 : DecisionEmail = DecisionEmail(decision=DecisionEnum.MAYBE, student=student06, date=date.today()) - decision_email7 : DecisionEmail = DecisionEmail(decision=DecisionEnum.MAYBE, student=student26, date=date.today()) - db.add(decision_email1) - db.add(decision_email2) - db.add(decision_email3) - db.add(decision_email4) - db.add(decision_email5) - db.add(decision_email6) - db.add(decision_email7) - db.commit() - - # InviteLink - inviteLink1: InviteLink = InviteLink(target_email="newuser1@email.com", edition=edition) - inviteLink2: InviteLink = InviteLink(target_email="newuser2@email.com", edition=edition) - db.add(inviteLink1) - db.add(inviteLink2) - db.commit() - - # Partner - partner1: Partner = Partner(name="Partner1") - partner2: Partner = Partner(name="Partner2") - partner3: Partner = Partner(name="Partner3") - db.add(partner1) - db.add(partner2) - db.add(partner3) - db.commit() - - # Project - project1: Project = Project(name="project1", number_of_students=3, edition=edition, partners=[partner1]) - project2: Project = Project(name="project2", number_of_students=6, edition=edition, partners=[partner2]) - project3: Project = Project(name="project3", number_of_students=2, edition=edition, partners=[partner3]) - project4: Project = Project(name="project4", number_of_students=9, edition=edition, partners=[partner1, partner3]) - db.add(project1) - db.add(project2) - db.add(project3) - db.add(project4) - db.commit() - - # Suggestion - suggestion: Suggestion = Suggestion(student=student01,coach=coach1, argumentation="Good student", suggestion=DecisionEnum.YES) - db.add(suggestion) - db.commit() \ No newline at end of file diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index f9afc7dcf..05a329fb4 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -1,9 +1,53 @@ import pytest from sqlalchemy.orm import Session from sqlalchemy.orm.exc import NoResultFound -from src.database.models import Student +from src.database.models import Suggestion, Student, User, Edition, Skill from src.database.crud.students import get_student_by_id -from tests.fill_database import fill_database + +def fill_database(db): + """A function to fill the database with fake data that can easly be used when testing""" + # Editions + edition: Edition = Edition(year=2022) + db.add(edition) + db.commit() + + # Users + admin: User = User(name="admin", email="admin@ngmail.com", admin=True) + coach1: User = User(name="coach1", email="coach1@noutlook.be") + coach2: User = User(name="coach2", email="coach2@noutlook.be") + request: User = User(name="request", email="request@ngmail.com") + db.add(admin) + db.add(coach1) + db.add(coach2) + db.add(request) + 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") + db.add(skill1) + db.add(skill2) + db.add(skill3) + db.add(skill4) + db.add(skill5) + db.add(skill6) + db.commit() + + # Student + 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, 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=True, + wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + + db.add(student01) + db.add(student30) + db.commit() def test_get_student_by_id(database_session: Session): """Tests if you get the right student""" diff --git a/backend/tests/test_database/test_crud/test_suggestions.py b/backend/tests/test_database/test_crud/test_suggestions.py index da4fa3ae3..ce9008bad 100644 --- a/backend/tests/test_database/test_crud/test_suggestions.py +++ b/backend/tests/test_database/test_crud/test_suggestions.py @@ -3,11 +3,62 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm.exc import NoResultFound -from src.database.models import Suggestion, Student, User +from src.database.models import Suggestion, Student, User, Edition, Skill -from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student, get_suggestion_by_id, delete_suggestion, update_suggestion +from src.database.crud.suggestions import ( create_suggestion, get_suggestions_of_student, + get_suggestion_by_id, delete_suggestion, update_suggestion ) from src.database.enums import DecisionEnum -from tests.fill_database import fill_database + +def fill_database(db): + """A function to fill the database with fake data that can easly be used when testing""" + # Editions + edition: Edition = Edition(year=2022) + db.add(edition) + db.commit() + + # Users + admin: User = User(name="admin", email="admin@ngmail.com", admin=True) + coach1: User = User(name="coach1", email="coach1@noutlook.be") + coach2: User = User(name="coach2", email="coach2@noutlook.be") + request: User = User(name="request", email="request@ngmail.com") + db.add(admin) + db.add(coach1) + db.add(coach2) + db.add(request) + 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") + db.add(skill1) + db.add(skill2) + db.add(skill3) + db.add(skill4) + db.add(skill5) + db.add(skill6) + db.commit() + + # Student + 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, 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=True, + wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + + db.add(student01) + db.add(student30) + db.commit() + + # Suggestion + suggestion1: Suggestion = Suggestion( + student=student01, coach=admin, argumentation="Good student", suggestion=DecisionEnum.YES) + db.add(suggestion1) + db.commit() def test_create_suggestion_yes(database_session: Session): @@ -88,7 +139,7 @@ def test_one_coach_two_students(database_session: Session): student1: Student = database_session.query(Student).where( Student.email_address == "marta.marquez@example.com").one() student2: Student = database_session.query(Student).where( - Student.email_address == "sofia.haataja@example.com").one() + Student.email_address == "josvermeulen@mail.com").one() create_suggestion(database_session, user.user_id, student1.student_id, DecisionEnum.YES, "This is a good student") @@ -154,7 +205,7 @@ def test_get_suggestion_by_id(database_session: Session): fill_database(database_session) suggestion: Suggestion = get_suggestion_by_id(database_session, 1) assert suggestion.student_id == 1 - assert suggestion.coach_id == 2 + assert suggestion.coach_id == 1 assert suggestion.suggestion == DecisionEnum.YES assert suggestion.argumentation == "Good student" 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 95eba8f58..a6c89a5be 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 @@ -2,8 +2,72 @@ from starlette import status from starlette.testclient import TestClient from src.database.enums import DecisionEnum -from src.database.models import Suggestion -from tests.fill_database import fill_database +from src.database.models import Suggestion, Student, User, Edition, Skill, AuthEmail +from src.app.logic.security import get_password_hash + + +def fill_database(db): + """A function to fill the database with fake data that can easly be used when testing""" + # Editions + edition: Edition = Edition(year=2022) + db.add(edition) + db.commit() + + # Users + admin: User = User(name="admin", email="admin@ngmail.com", admin=True) + coach1: User = User(name="coach1", email="coach1@noutlook.be") + coach2: User = User(name="coach2", email="coach2@noutlook.be") + request: User = User(name="request", email="request@ngmail.com") + db.add(admin) + db.add(coach1) + db.add(coach2) + db.add(request) + db.commit() + + # AuthEmail + pw_hash = get_password_hash("wachtwoord") + auth_email_admin: AuthEmail = AuthEmail(user=admin, pw_hash=pw_hash) + auth_email_coach1: AuthEmail = AuthEmail(user=coach1, pw_hash=pw_hash) + auth_email_coach2: AuthEmail = AuthEmail(user=coach2, pw_hash=pw_hash) + auth_email_request: AuthEmail = AuthEmail(user=request, pw_hash=pw_hash) + db.add(auth_email_admin) + db.add(auth_email_coach1) + db.add(auth_email_coach2) + db.add(auth_email_request) + 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") + db.add(skill1) + db.add(skill2) + db.add(skill3) + db.add(skill4) + db.add(skill5) + db.add(skill6) + db.commit() + + # Student + 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, 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=True, + wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + + db.add(student01) + db.add(student30) + db.commit() + + # Suggestion + suggestion1: Suggestion = Suggestion( + student=student01, coach=coach1, argumentation="Good student", suggestion=DecisionEnum.YES) + db.add(suggestion1) + db.commit() def test_new_suggestion(database_session: Session, test_client: TestClient): @@ -18,11 +82,11 @@ def test_new_suggestion(database_session: Session, test_client: TestClient): } token = test_client.post("/login/token", data=form).json()["accessToken"] auth = "Bearer " + token - resp = test_client.post("/editions/1/students/29/suggestions/", headers={ + resp = test_client.post("/editions/1/students/2/suggestions/", headers={ "Authorization": auth}, json={"suggestion": 1, "argumentation": "test"}) assert resp.status_code == status.HTTP_201_CREATED suggestions: list[Suggestion] = database_session.query( - Suggestion).where(Suggestion.student_id == 29).all() + Suggestion).where(Suggestion.student_id == 2).all() assert len(suggestions) == 1 print(resp.json()) assert resp.json()[ @@ -37,7 +101,7 @@ def test_new_suggestion_not_authorized(database_session: Session, test_client: T """Tests when not authorized you can't add a new suggestion""" fill_database(database_session) - assert test_client.post("/editions/1/students/29/suggestions/", json={ + assert test_client.post("/editions/1/students/2/suggestions/", json={ "suggestion": 1, "argumentation": "test"}).status_code == status.HTTP_401_UNAUTHORIZED suggestions: list[Suggestion] = database_session.query( Suggestion).where(Suggestion.student_id == 29).all() @@ -78,7 +142,7 @@ def test_get_suggestions_of_student(database_session: Session, test_client: Test } token = test_client.post("/login/token", data=form).json()["accessToken"] auth = "Bearer " + token - assert test_client.post("/editions/1/students/29/suggestions/", headers={"Authorization": auth}, json={ + assert test_client.post("/editions/1/students/2/suggestions/", headers={"Authorization": auth}, json={ "suggestion": 1, "argumentation": "Ja"}).status_code == status.HTTP_201_CREATED form = { "username": "admin@ngmail.com", @@ -86,10 +150,10 @@ def test_get_suggestions_of_student(database_session: Session, test_client: Test } token = test_client.post("/login/token", data=form).json()["accessToken"] auth = "Bearer " + token - assert test_client.post("/editions/1/students/29/suggestions/", headers={"Authorization": auth}, json={ + assert test_client.post("/editions/1/students/2/suggestions/", headers={"Authorization": auth}, json={ "suggestion": 3, "argumentation": "Neen"}).status_code == status.HTTP_201_CREATED res = test_client.get( - "/editions/1/students/29/suggestions/", headers={"Authorization": auth}) + "/editions/1/students/2/suggestions/", headers={"Authorization": auth}) assert res.status_code == status.HTTP_200_OK res_json = res.json() assert len(res_json["suggestions"]) == 2 From 0aa72e9a9be713084988c73cec729f2a9572b75b Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Mon, 21 Mar 2022 15:02:10 +0100 Subject: [PATCH 069/536] added fixture --- .../test_database/test_crud/test_students.py | 51 +++--- .../test_crud/test_suggestions.py | 157 +++++++++--------- .../test_suggestions/test_suggestions.py | 115 ++++++------- 3 files changed, 154 insertions(+), 169 deletions(-) diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index 05a329fb4..a1c1fb434 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -1,26 +1,28 @@ import pytest from sqlalchemy.orm import Session from sqlalchemy.orm.exc import NoResultFound -from src.database.models import Suggestion, Student, User, Edition, Skill +from src.database.models import Student, User, Edition, Skill from src.database.crud.students import get_student_by_id -def fill_database(db): + +@pytest.fixture +def database_with_data(database_session: Session): """A function to fill the database with fake data that can easly be used when testing""" # Editions edition: Edition = Edition(year=2022) - db.add(edition) - db.commit() + database_session.add(edition) + database_session.commit() # Users admin: User = User(name="admin", email="admin@ngmail.com", admin=True) coach1: User = User(name="coach1", email="coach1@noutlook.be") coach2: User = User(name="coach2", email="coach2@noutlook.be") request: User = User(name="request", email="request@ngmail.com") - db.add(admin) - db.add(coach1) - db.add(coach2) - db.add(request) - db.commit() + database_session.add(admin) + database_session.add(coach1) + database_session.add(coach2) + database_session.add(request) + database_session.commit() # Skill skill1: Skill = Skill(name="skill1", description="something about skill1") @@ -29,13 +31,13 @@ def fill_database(db): 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") - db.add(skill1) - db.add(skill2) - db.add(skill3) - db.add(skill4) - db.add(skill5) - db.add(skill6) - db.commit() + 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", @@ -45,21 +47,22 @@ def fill_database(db): email_address="marta.marquez@example.com", phone_number="967-895-285", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - db.add(student01) - db.add(student30) - db.commit() + database_session.add(student01) + database_session.add(student30) + database_session.commit() + + return database_session -def test_get_student_by_id(database_session: Session): +def test_get_student_by_id(database_with_data: Session): """Tests if you get the right student""" - fill_database(database_session) - student: Student = get_student_by_id(database_session, 1) + student: Student = 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_session: Session): +def test_no_student(database_with_data: Session): """Tests if you get an error for a not existing student""" with pytest.raises(NoResultFound): - get_student_by_id(database_session, 5) + get_student_by_id(database_with_data, 5) diff --git a/backend/tests/test_database/test_crud/test_suggestions.py b/backend/tests/test_database/test_crud/test_suggestions.py index ce9008bad..5501d91ff 100644 --- a/backend/tests/test_database/test_crud/test_suggestions.py +++ b/backend/tests/test_database/test_crud/test_suggestions.py @@ -9,23 +9,24 @@ get_suggestion_by_id, delete_suggestion, update_suggestion ) from src.database.enums import DecisionEnum -def fill_database(db): +@pytest.fixture +def database_with_data(database_session: Session): """A function to fill the database with fake data that can easly be used when testing""" # Editions edition: Edition = Edition(year=2022) - db.add(edition) - db.commit() + database_session.add(edition) + database_session.commit() # Users admin: User = User(name="admin", email="admin@ngmail.com", admin=True) coach1: User = User(name="coach1", email="coach1@noutlook.be") coach2: User = User(name="coach2", email="coach2@noutlook.be") request: User = User(name="request", email="request@ngmail.com") - db.add(admin) - db.add(coach1) - db.add(coach2) - db.add(request) - db.commit() + database_session.add(admin) + database_session.add(coach1) + database_session.add(coach2) + database_session.add(request) + database_session.commit() # Skill skill1: Skill = Skill(name="skill1", description="something about skill1") @@ -34,13 +35,13 @@ def fill_database(db): 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") - db.add(skill1) - db.add(skill2) - db.add(skill3) - db.add(skill4) - db.add(skill5) - db.add(skill6) - db.commit() + 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", @@ -50,30 +51,30 @@ def fill_database(db): email_address="marta.marquez@example.com", phone_number="967-895-285", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - db.add(student01) - db.add(student30) - db.commit() + database_session.add(student01) + database_session.add(student30) + database_session.commit() # Suggestion suggestion1: Suggestion = Suggestion( student=student01, coach=admin, argumentation="Good student", suggestion=DecisionEnum.YES) - db.add(suggestion1) - db.commit() + database_session.add(suggestion1) + database_session.commit() + return database_session -def test_create_suggestion_yes(database_session: Session): +def test_create_suggestion_yes(database_with_data: Session): """Test creat a yes suggestion""" - fill_database(database_session) - user: User = database_session.query( + user: User = database_with_data.query( User).where(User.name == "coach1").first() - student: Student = database_session.query(Student).where( + student: Student = database_with_data.query(Student).where( Student.email_address == "marta.marquez@example.com").first() new_suggestion = create_suggestion( - database_session, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") + database_with_data, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") - suggestion: Suggestion = database_session.query(Suggestion).where( + suggestion: Suggestion = database_with_data.query(Suggestion).where( Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() assert new_suggestion == suggestion @@ -84,19 +85,18 @@ def test_create_suggestion_yes(database_session: Session): assert suggestion.argumentation == "This is a good student" -def test_create_suggestion_no(database_session: Session): +def test_create_suggestion_no(database_with_data: Session): """Test create a no suggestion""" - fill_database(database_session) - user: User = database_session.query( + user: User = database_with_data.query( User).where(User.name == "coach1").first() - student: Student = database_session.query(Student).where( + student: Student = database_with_data.query(Student).where( Student.email_address == "marta.marquez@example.com").first() new_suggestion = create_suggestion( - database_session, user.user_id, student.student_id, DecisionEnum.NO, "This is a not good student") + database_with_data, user.user_id, student.student_id, DecisionEnum.NO, "This is a not good student") - suggestion: Suggestion = database_session.query(Suggestion).where( + suggestion: Suggestion = database_with_data.query(Suggestion).where( Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() assert new_suggestion == suggestion @@ -107,19 +107,18 @@ def test_create_suggestion_no(database_session: Session): assert suggestion.argumentation == "This is a not good student" -def test_create_suggestion_maybe(database_session: Session): +def test_create_suggestion_maybe(database_with_data: Session): """Test create a maybe suggestion""" - fill_database(database_session) - user: User = database_session.query( + user: User = database_with_data.query( User).where(User.name == "coach1").first() - student: Student = database_session.query(Student).where( + student: Student = database_with_data.query(Student).where( Student.email_address == "marta.marquez@example.com").first() new_suggestion = create_suggestion( - database_session, user.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") + database_with_data, user.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") - suggestion: Suggestion = database_session.query(Suggestion).where( + suggestion: Suggestion = database_with_data.query(Suggestion).where( Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() assert new_suggestion == suggestion @@ -130,30 +129,29 @@ def test_create_suggestion_maybe(database_session: Session): assert suggestion.argumentation == "Idk if it's good student" -def test_one_coach_two_students(database_session: Session): +def test_one_coach_two_students(database_with_data: Session): """Test that one coach can write multiple suggestions""" - fill_database(database_session) - user: User = database_session.query( + user: User = database_with_data.query( User).where(User.name == "coach1").one() - student1: Student = database_session.query(Student).where( + student1: Student = database_with_data.query(Student).where( Student.email_address == "marta.marquez@example.com").one() - student2: Student = database_session.query(Student).where( + student2: Student = database_with_data.query(Student).where( Student.email_address == "josvermeulen@mail.com").one() - create_suggestion(database_session, user.user_id, + create_suggestion(database_with_data, user.user_id, student1.student_id, DecisionEnum.YES, "This is a good student") - create_suggestion(database_session, user.user_id, student2.student_id, + create_suggestion(database_with_data, user.user_id, student2.student_id, DecisionEnum.NO, "This is a not good student") - suggestion1: Suggestion = database_session.query(Suggestion).where( + suggestion1: Suggestion = database_with_data.query(Suggestion).where( Suggestion.coach == user).where(Suggestion.student_id == student1.student_id).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_session.query(Suggestion).where( + suggestion2: Suggestion = database_with_data.query(Suggestion).where( Suggestion.coach == user).where(Suggestion.student_id == student2.student_id).one() assert suggestion2.coach == user assert suggestion2.student == student2 @@ -161,100 +159,95 @@ def test_one_coach_two_students(database_session: Session): assert suggestion2.argumentation == "This is a not good student" -def test_multiple_suggestions_about_same_student(database_session: Session): +def test_multiple_suggestions_about_same_student(database_with_data: Session): """Test get multiple suggestions about the same student""" - fill_database(database_session) - user: User = database_session.query( + user: User = database_with_data.query( User).where(User.name == "coach1").first() - student: Student = database_session.query(Student).where( + student: Student = database_with_data.query(Student).where( Student.email_address == "marta.marquez@example.com").first() - create_suggestion(database_session, user.user_id, student.student_id, + 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_session, user.user_id, + 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_session: Session): +def test_get_suggestions_of_student(database_with_data: Session): """Test get all suggestions of a student""" - fill_database(database_session) - user1: User = database_session.query( + user1: User = database_with_data.query( User).where(User.name == "coach1").first() - user2: User = database_session.query( + user2: User = database_with_data.query( User).where(User.name == "coach2").first() - student: Student = database_session.query(Student).where( + student: Student = database_with_data.query(Student).where( Student.email_address == "marta.marquez@example.com").first() - create_suggestion(database_session, user1.user_id, student.student_id, + create_suggestion(database_with_data, user1.user_id, student.student_id, DecisionEnum.MAYBE, "Idk if it's good student") - create_suggestion(database_session, user2.user_id, + create_suggestion(database_with_data, user2.user_id, student.student_id, DecisionEnum.YES, "This is a good student") suggestions_student = get_suggestions_of_student( - database_session, student.student_id) + database_with_data, student.student_id) assert len(suggestions_student) == 2 assert suggestions_student[0].student == student assert suggestions_student[1].student == student -def test_get_suggestion_by_id(database_session: Session): +def test_get_suggestion_by_id(database_with_data: Session): """Test get suggestion by id""" - fill_database(database_session) - suggestion: Suggestion = get_suggestion_by_id(database_session, 1) + suggestion: Suggestion = 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_session: Session): +def test_get_suggestion_by_id_non_existing(database_with_data: Session): """Test you get an error when you search an id that don't exist""" with pytest.raises(NoResultFound): - get_suggestion_by_id(database_session, 1) + get_suggestion_by_id(database_with_data, 900) -def test_delete_suggestion(database_session: Session): +def test_delete_suggestion(database_with_data: Session): """Test delete suggestion""" - fill_database(database_session) - user: User = database_session.query( + user: User = database_with_data.query( User).where(User.name == "coach1").first() - student: Student = database_session.query(Student).where( + student: Student = database_with_data.query(Student).where( Student.email_address == "marta.marquez@example.com").first() - create_suggestion(database_session, user.user_id, + create_suggestion(database_with_data, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") - suggestion: Suggestion = database_session.query(Suggestion).where( + suggestion: Suggestion = database_with_data.query(Suggestion).where( Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() - delete_suggestion(database_session, suggestion) + delete_suggestion(database_with_data, suggestion) - suggestions: list[Suggestion] = database_session.query(Suggestion).where( + suggestions: list[Suggestion] = database_with_data.query(Suggestion).where( Suggestion.coach == user).where(Suggestion.student_id == student.student_id).all() assert len(suggestions) == 0 -def test_update_suggestion(database_session: Session): +def test_update_suggestion(database_with_data: Session): """Test update suggestion""" - fill_database(database_session) - user: User = database_session.query( + user: User = database_with_data.query( User).where(User.name == "coach1").first() - student: Student = database_session.query(Student).where( + student: Student = database_with_data.query(Student).where( Student.email_address == "marta.marquez@example.com").first() - create_suggestion(database_session, user.user_id, + create_suggestion(database_with_data, user.user_id, student.student_id, DecisionEnum.YES, "This is a good student") - suggestion: Suggestion = database_session.query(Suggestion).where( + suggestion: Suggestion = database_with_data.query(Suggestion).where( Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() - update_suggestion(database_session, suggestion, + update_suggestion(database_with_data, suggestion, DecisionEnum.NO, "Not that good student") - new_suggestion: Suggestion = database_session.query(Suggestion).where( + new_suggestion: Suggestion = database_with_data.query(Suggestion).where( Suggestion.coach == user).where(Suggestion.student_id == student.student_id).one() assert new_suggestion.suggestion == DecisionEnum.NO assert new_suggestion.argumentation == "Not that good student" 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 a6c89a5be..0e9dbdc24 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,3 +1,4 @@ +import pytest from sqlalchemy.orm import Session from starlette import status from starlette.testclient import TestClient @@ -6,23 +7,24 @@ from src.app.logic.security import get_password_hash -def fill_database(db): +@pytest.fixture +def database_with_data(database_session: Session): """A function to fill the database with fake data that can easly be used when testing""" # Editions edition: Edition = Edition(year=2022) - db.add(edition) - db.commit() + database_session.add(edition) + database_session.commit() # Users admin: User = User(name="admin", email="admin@ngmail.com", admin=True) coach1: User = User(name="coach1", email="coach1@noutlook.be") coach2: User = User(name="coach2", email="coach2@noutlook.be") request: User = User(name="request", email="request@ngmail.com") - db.add(admin) - db.add(coach1) - db.add(coach2) - db.add(request) - db.commit() + database_session.add(admin) + database_session.add(coach1) + database_session.add(coach2) + database_session.add(request) + database_session.commit() # AuthEmail pw_hash = get_password_hash("wachtwoord") @@ -30,11 +32,11 @@ def fill_database(db): auth_email_coach1: AuthEmail = AuthEmail(user=coach1, pw_hash=pw_hash) auth_email_coach2: AuthEmail = AuthEmail(user=coach2, pw_hash=pw_hash) auth_email_request: AuthEmail = AuthEmail(user=request, pw_hash=pw_hash) - db.add(auth_email_admin) - db.add(auth_email_coach1) - db.add(auth_email_coach2) - db.add(auth_email_request) - db.commit() + database_session.add(auth_email_admin) + database_session.add(auth_email_coach1) + database_session.add(auth_email_coach2) + database_session.add(auth_email_request) + database_session.commit() # Skill skill1: Skill = Skill(name="skill1", description="something about skill1") @@ -43,13 +45,13 @@ def fill_database(db): 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") - db.add(skill1) - db.add(skill2) - db.add(skill3) - db.add(skill4) - db.add(skill5) - db.add(skill6) - db.commit() + 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", @@ -59,21 +61,21 @@ def fill_database(db): email_address="marta.marquez@example.com", phone_number="967-895-285", alumni=True, wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) - db.add(student01) - db.add(student30) - db.commit() + database_session.add(student01) + database_session.add(student30) + database_session.commit() # Suggestion suggestion1: Suggestion = Suggestion( student=student01, coach=coach1, argumentation="Good student", suggestion=DecisionEnum.YES) - db.add(suggestion1) - db.commit() + database_session.add(suggestion1) + database_session.commit() + return database_session -def test_new_suggestion(database_session: Session, test_client: TestClient): +def test_new_suggestion(database_with_data: Session, test_client: TestClient): """Tests a new sugesstion""" - fill_database(database_session) email = "coach1@noutlook.be" password = "wachtwoord" form = { @@ -85,7 +87,7 @@ def test_new_suggestion(database_session: Session, test_client: TestClient): resp = test_client.post("/editions/1/students/2/suggestions/", headers={ "Authorization": auth}, json={"suggestion": 1, "argumentation": "test"}) assert resp.status_code == status.HTTP_201_CREATED - suggestions: list[Suggestion] = database_session.query( + suggestions: list[Suggestion] = database_with_data.query( Suggestion).where(Suggestion.student_id == 2).all() assert len(suggestions) == 1 print(resp.json()) @@ -97,28 +99,26 @@ def test_new_suggestion(database_session: Session, test_client: TestClient): "suggestion"]["argumentation"] == suggestions[0].argumentation -def test_new_suggestion_not_authorized(database_session: Session, test_client: TestClient): +def test_new_suggestion_not_authorized(database_with_data: Session, test_client: TestClient): """Tests when not authorized you can't add a new suggestion""" - fill_database(database_session) assert test_client.post("/editions/1/students/2/suggestions/", json={ "suggestion": 1, "argumentation": "test"}).status_code == status.HTTP_401_UNAUTHORIZED - suggestions: list[Suggestion] = database_session.query( - Suggestion).where(Suggestion.student_id == 29).all() + suggestions: list[Suggestion] = database_with_data.query( + Suggestion).where(Suggestion.student_id == 2).all() assert len(suggestions) == 0 -def test_get_suggestions_of_student_not_authorized(database_session: Session, test_client: TestClient): +def test_get_suggestions_of_student_not_authorized(database_with_data: Session, test_client: TestClient): """Tests if you don't have the right access, you get the right HTTP code""" assert test_client.get("/editions/1/students/29/suggestions/", headers={"Authorization": "auth"}, json={ "suggestion": 1, "argumentation": "Ja"}).status_code == status.HTTP_401_UNAUTHORIZED -def test_get_suggestions_of_ghost(database_session: Session, test_client: TestClient): +def test_get_suggestions_of_ghost(database_with_data: Session, test_client: TestClient): """Tests if the student don't exist, you get a 404""" - fill_database(database_session) email = "coach1@noutlook.be" password = "wachtwoord" form = { @@ -132,10 +132,9 @@ def test_get_suggestions_of_ghost(database_session: Session, test_client: TestCl assert res.status_code == status.HTTP_404_NOT_FOUND -def test_get_suggestions_of_student(database_session: Session, test_client: TestClient): +def test_get_suggestions_of_student(database_with_data: Session, test_client: TestClient): """Tests to get the suggestions of a student""" - fill_database(database_session) form = { "username": "coach1@noutlook.be", "password": "wachtwoord" @@ -165,9 +164,8 @@ def test_get_suggestions_of_student(database_session: Session, test_client: Test assert res_json["suggestions"][1]["argumentation"] == "Neen" -def test_delete_ghost_suggestion(database_session: Session, test_client: TestClient): +def test_delete_ghost_suggestion(database_with_data: Session, test_client: TestClient): """Tests that you get the correct status code when you delete a not existing suggestion""" - fill_database(database_session) form = { "username": "admin@ngmail.com", "password": "wachtwoord" @@ -178,16 +176,14 @@ def test_delete_ghost_suggestion(database_session: Session, test_client: TestCli "Authorization": auth}).status_code == status.HTTP_404_NOT_FOUND -def test_delete_not_autorized(database_session: Session, test_client: TestClient): +def test_delete_not_autorized(database_with_data: Session, test_client: TestClient): """Tests that you have to be loged in for deleating a suggestion""" - fill_database(database_session) assert test_client.delete("/editions/1/students/1/suggestions/8000", headers={ "Authorization": "auth"}).status_code == status.HTTP_401_UNAUTHORIZED -def test_delete_suggestion_admin(database_session: Session, test_client: TestClient): +def test_delete_suggestion_admin(database_with_data: Session, test_client: TestClient): """Test that an admin can update suggestions""" - fill_database(database_session) form = { "username": "admin@ngmail.com", "password": "wachtwoord" @@ -196,14 +192,13 @@ def test_delete_suggestion_admin(database_session: Session, test_client: TestCli auth = "Bearer " + token assert test_client.delete("/editions/1/students/1/suggestions/1", headers={ "Authorization": auth}).status_code == status.HTTP_204_NO_CONTENT - suggestions: Suggestion = database_session.query( + suggestions: Suggestion = database_with_data.query( Suggestion).where(Suggestion.suggestion_id == 1).all() assert len(suggestions) == 0 -def test_delete_suggestion_coach_their_review(database_session: Session, test_client: TestClient): +def test_delete_suggestion_coach_their_review(database_with_data: Session, test_client: TestClient): """Tests that a coach can delete their own suggestion""" - fill_database(database_session) form = { "username": "coach1@noutlook.be", "password": "wachtwoord" @@ -212,14 +207,13 @@ def test_delete_suggestion_coach_their_review(database_session: Session, test_cl auth = "Bearer " + token assert test_client.delete("/editions/1/students/1/suggestions/1", headers={ "Authorization": auth}).status_code == status.HTTP_204_NO_CONTENT - suggestions: Suggestion = database_session.query( + suggestions: Suggestion = database_with_data.query( Suggestion).where(Suggestion.suggestion_id == 1).all() assert len(suggestions) == 0 -def test_delete_suggestion_coach_other_review(database_session: Session, test_client: TestClient): +def test_delete_suggestion_coach_other_review(database_with_data: Session, test_client: TestClient): """Tests that a coach can't delete other coaches their suggestions""" - fill_database(database_session) form = { "username": "coach2@noutlook.be", "password": "wachtwoord" @@ -228,14 +222,13 @@ def test_delete_suggestion_coach_other_review(database_session: Session, test_cl auth = "Bearer " + token assert test_client.delete("/editions/1/students/1/suggestions/1", headers={ "Authorization": auth}).status_code == status.HTTP_403_FORBIDDEN - suggestions: Suggestion = database_session.query( + suggestions: Suggestion = database_with_data.query( Suggestion).where(Suggestion.suggestion_id == 1).all() assert len(suggestions) == 1 -def test_update_ghost_suggestion(database_session: Session, test_client: TestClient): +def test_update_ghost_suggestion(database_with_data: Session, test_client: TestClient): """Tests a suggestion that don't exist """ - fill_database(database_session) form = { "username": "admin@ngmail.com", "password": "wachtwoord" @@ -246,16 +239,14 @@ def test_update_ghost_suggestion(database_session: Session, test_client: TestCli "suggestion": 1, "argumentation": "test"}).status_code == status.HTTP_404_NOT_FOUND -def test_update_not_autorized(database_session: Session, test_client: TestClient): +def test_update_not_autorized(database_with_data: Session, test_client: TestClient): """Tests update when not autorized""" - fill_database(database_session) assert test_client.put("/editions/1/students/1/suggestions/8000", headers={"Authorization": "auth"}, json={ "suggestion": 1, "argumentation": "test"}).status_code == status.HTTP_401_UNAUTHORIZED -def test_update_suggestion_admin(database_session: Session, test_client: TestClient): +def test_update_suggestion_admin(database_with_data: Session, test_client: TestClient): """Test that an admin can update suggestions""" - fill_database(database_session) form = { "username": "admin@ngmail.com", "password": "wachtwoord" @@ -264,15 +255,14 @@ def test_update_suggestion_admin(database_session: Session, test_client: TestCli auth = "Bearer " + token assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}, json={ "suggestion": 3, "argumentation": "test"}).status_code == status.HTTP_204_NO_CONTENT - suggestion: Suggestion = database_session.query( + suggestion: Suggestion = database_with_data.query( Suggestion).where(Suggestion.suggestion_id == 1).one() assert suggestion.suggestion == DecisionEnum.NO assert suggestion.argumentation == "test" -def test_update_suggestion_coach_their_review(database_session: Session, test_client: TestClient): +def test_update_suggestion_coach_their_review(database_with_data: Session, test_client: TestClient): """Tests that a coach can update their own suggestion""" - fill_database(database_session) form = { "username": "coach1@noutlook.be", "password": "wachtwoord" @@ -281,15 +271,14 @@ def test_update_suggestion_coach_their_review(database_session: Session, test_cl auth = "Bearer " + token assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}, json={ "suggestion": 3, "argumentation": "test"}).status_code == status.HTTP_204_NO_CONTENT - suggestion: Suggestion = database_session.query( + suggestion: Suggestion = database_with_data.query( Suggestion).where(Suggestion.suggestion_id == 1).one() assert suggestion.suggestion == DecisionEnum.NO assert suggestion.argumentation == "test" -def test_update_suggestion_coach_other_review(database_session: Session, test_client: TestClient): +def test_update_suggestion_coach_other_review(database_with_data: Session, test_client: TestClient): """Tests that a coach can't update other coaches their suggestions""" - fill_database(database_session) form = { "username": "coach2@noutlook.be", "password": "wachtwoord" @@ -298,7 +287,7 @@ def test_update_suggestion_coach_other_review(database_session: Session, test_cl auth = "Bearer " + token assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}, json={ "suggestion": 3, "argumentation": "test"}).status_code == status.HTTP_403_FORBIDDEN - suggestion: Suggestion = database_session.query( + suggestion: Suggestion = database_with_data.query( Suggestion).where(Suggestion.suggestion_id == 1).one() assert suggestion.suggestion != DecisionEnum.NO assert suggestion.argumentation != "test" From bc29572d2b4b95d14ea2872b896e4d5716c3efe0 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Mon, 21 Mar 2022 15:14:29 +0100 Subject: [PATCH 070/536] made authorization a fixture --- .../test_suggestions/test_suggestions.py | 170 ++++++++---------- 1 file changed, 70 insertions(+), 100 deletions(-) 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 0e9dbdc24..ff48eaf0b 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 @@ -8,8 +8,9 @@ @pytest.fixture -def database_with_data(database_session: Session): - """A function to fill the database with fake data that can easly be used when testing""" +def database_with_data(database_session: Session) -> Session: + """A fixture to fill the database with fake data that can easly be used when testing""" + # Editions edition: Edition = Edition(year=2022) database_session.add(edition) @@ -73,19 +74,48 @@ def database_with_data(database_session: Session): return database_session -def test_new_suggestion(database_with_data: Session, test_client: TestClient): - """Tests a new sugesstion""" +@pytest.fixture +def auth_coach1(test_client: TestClient) -> str: + """A fixture for logging in coach1""" - email = "coach1@noutlook.be" - password = "wachtwoord" form = { - "username": email, - "password": password + "username": "coach1@noutlook.be", + "password": "wachtwoord" } token = test_client.post("/login/token", data=form).json()["accessToken"] auth = "Bearer " + token + return auth + +@pytest.fixture +def auth_coach2(test_client: TestClient) -> str: + """A fixture for logging in coach1""" + + form = { + "username": "coach2@noutlook.be", + "password": "wachtwoord" + } + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + return auth + +@pytest.fixture +def auth_admin(test_client: TestClient) -> str: + """A fixture for logging in admin""" + + form = { + "username": "admin@ngmail.com", + "password": "wachtwoord" + } + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + return auth + + +def test_new_suggestion(database_with_data: Session, test_client: TestClient, auth_coach1): + """Tests a new sugesstion""" + resp = test_client.post("/editions/1/students/2/suggestions/", headers={ - "Authorization": auth}, json={"suggestion": 1, "argumentation": "test"}) + "Authorization": auth_coach1}, 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() @@ -102,7 +132,7 @@ def test_new_suggestion(database_with_data: Session, test_client: TestClient): def test_new_suggestion_not_authorized(database_with_data: Session, test_client: TestClient): """Tests when not authorized you can't add a new suggestion""" - assert test_client.post("/editions/1/students/2/suggestions/", json={ + assert test_client.post("/editions/1/students/2/suggestions/", headers={"Authorization": "auth"}, 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() @@ -116,43 +146,24 @@ def test_get_suggestions_of_student_not_authorized(database_with_data: Session, "suggestion": 1, "argumentation": "Ja"}).status_code == status.HTTP_401_UNAUTHORIZED -def test_get_suggestions_of_ghost(database_with_data: Session, test_client: TestClient): +def test_get_suggestions_of_ghost(database_with_data: Session, test_client: TestClient, auth_coach1: str): """Tests if the student don't exist, you get a 404""" - email = "coach1@noutlook.be" - password = "wachtwoord" - form = { - "username": email, - "password": password - } - token = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token res = test_client.get( - "/editions/1/students/9000/suggestions/", headers={"Authorization": auth}) + "/editions/1/students/9000/suggestions/", headers={"Authorization": auth_coach1}) assert res.status_code == status.HTTP_404_NOT_FOUND -def test_get_suggestions_of_student(database_with_data: Session, test_client: TestClient): +def test_get_suggestions_of_student(database_with_data: Session, test_client: TestClient, auth_coach1: str, auth_admin: str): """Tests to get the suggestions of a student""" - form = { - "username": "coach1@noutlook.be", - "password": "wachtwoord" - } - token = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token - assert test_client.post("/editions/1/students/2/suggestions/", headers={"Authorization": auth}, json={ + assert test_client.post("/editions/1/students/2/suggestions/", headers={"Authorization": auth_coach1}, json={ "suggestion": 1, "argumentation": "Ja"}).status_code == status.HTTP_201_CREATED - form = { - "username": "admin@ngmail.com", - "password": "wachtwoord" - } - token = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token - assert test_client.post("/editions/1/students/2/suggestions/", headers={"Authorization": auth}, json={ + + assert test_client.post("/editions/1/students/2/suggestions/", headers={"Authorization": auth_admin}, json={ "suggestion": 3, "argumentation": "Neen"}).status_code == status.HTTP_201_CREATED res = test_client.get( - "/editions/1/students/2/suggestions/", headers={"Authorization": auth}) + "/editions/1/students/2/suggestions/", headers={"Authorization": auth_admin}) assert res.status_code == status.HTTP_200_OK res_json = res.json() assert len(res_json["suggestions"]) == 2 @@ -164,16 +175,10 @@ def test_get_suggestions_of_student(database_with_data: Session, test_client: Te assert res_json["suggestions"][1]["argumentation"] == "Neen" -def test_delete_ghost_suggestion(database_with_data: Session, test_client: TestClient): +def test_delete_ghost_suggestion(database_with_data: Session, test_client: TestClient, auth_coach1: str): """Tests that you get the correct status code when you delete a not existing suggestion""" - form = { - "username": "admin@ngmail.com", - "password": "wachtwoord" - } - token = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token assert test_client.delete("/editions/1/students/1/suggestions/8000", headers={ - "Authorization": auth}).status_code == status.HTTP_404_NOT_FOUND + "Authorization": auth_coach1}).status_code == status.HTTP_404_NOT_FOUND def test_delete_not_autorized(database_with_data: Session, test_client: TestClient): @@ -182,60 +187,40 @@ def test_delete_not_autorized(database_with_data: Session, test_client: TestClie "Authorization": "auth"}).status_code == status.HTTP_401_UNAUTHORIZED -def test_delete_suggestion_admin(database_with_data: Session, test_client: TestClient): +def test_delete_suggestion_admin(database_with_data: Session, test_client: TestClient, auth_admin: str): """Test that an admin can update suggestions""" - form = { - "username": "admin@ngmail.com", - "password": "wachtwoord" - } - token = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token + assert test_client.delete("/editions/1/students/1/suggestions/1", headers={ - "Authorization": auth}).status_code == status.HTTP_204_NO_CONTENT + "Authorization": auth_admin}).status_code == status.HTTP_204_NO_CONTENT suggestions: Suggestion = database_with_data.query( Suggestion).where(Suggestion.suggestion_id == 1).all() assert len(suggestions) == 0 -def test_delete_suggestion_coach_their_review(database_with_data: Session, test_client: TestClient): +def test_delete_suggestion_coach_their_review(database_with_data: Session, test_client: TestClient, auth_coach1: str): """Tests that a coach can delete their own suggestion""" - form = { - "username": "coach1@noutlook.be", - "password": "wachtwoord" - } - token = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token + assert test_client.delete("/editions/1/students/1/suggestions/1", headers={ - "Authorization": auth}).status_code == status.HTTP_204_NO_CONTENT + "Authorization": auth_coach1}).status_code == status.HTTP_204_NO_CONTENT suggestions: Suggestion = database_with_data.query( Suggestion).where(Suggestion.suggestion_id == 1).all() assert len(suggestions) == 0 -def test_delete_suggestion_coach_other_review(database_with_data: Session, test_client: TestClient): +def test_delete_suggestion_coach_other_review(database_with_data: Session, test_client: TestClient, auth_coach2: str): """Tests that a coach can't delete other coaches their suggestions""" - form = { - "username": "coach2@noutlook.be", - "password": "wachtwoord" - } - token = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token + assert test_client.delete("/editions/1/students/1/suggestions/1", headers={ - "Authorization": auth}).status_code == status.HTTP_403_FORBIDDEN + "Authorization": auth_coach2}).status_code == status.HTTP_403_FORBIDDEN suggestions: Suggestion = database_with_data.query( Suggestion).where(Suggestion.suggestion_id == 1).all() assert len(suggestions) == 1 -def test_update_ghost_suggestion(database_with_data: Session, test_client: TestClient): +def test_update_ghost_suggestion(database_with_data: Session, test_client: TestClient, auth_admin: str): """Tests a suggestion that don't exist """ - form = { - "username": "admin@ngmail.com", - "password": "wachtwoord" - } - token = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token - assert test_client.put("/editions/1/students/1/suggestions/8000", headers={"Authorization": auth}, json={ + + assert test_client.put("/editions/1/students/1/suggestions/8000", headers={"Authorization": auth_admin}, json={ "suggestion": 1, "argumentation": "test"}).status_code == status.HTTP_404_NOT_FOUND @@ -245,15 +230,10 @@ def test_update_not_autorized(database_with_data: Session, test_client: TestClie "suggestion": 1, "argumentation": "test"}).status_code == status.HTTP_401_UNAUTHORIZED -def test_update_suggestion_admin(database_with_data: Session, test_client: TestClient): +def test_update_suggestion_admin(database_with_data: Session, test_client: TestClient, auth_admin: str): """Test that an admin can update suggestions""" - form = { - "username": "admin@ngmail.com", - "password": "wachtwoord" - } - token = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token - assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}, json={ + + assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth_admin}, 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() @@ -261,15 +241,10 @@ def test_update_suggestion_admin(database_with_data: Session, test_client: TestC assert suggestion.argumentation == "test" -def test_update_suggestion_coach_their_review(database_with_data: Session, test_client: TestClient): +def test_update_suggestion_coach_their_review(database_with_data: Session, test_client: TestClient, auth_coac1: str): """Tests that a coach can update their own suggestion""" - form = { - "username": "coach1@noutlook.be", - "password": "wachtwoord" - } - token = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token - assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}, json={ + + assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth_coach1}, 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() @@ -277,15 +252,10 @@ def test_update_suggestion_coach_their_review(database_with_data: Session, test_ assert suggestion.argumentation == "test" -def test_update_suggestion_coach_other_review(database_with_data: Session, test_client: TestClient): +def test_update_suggestion_coach_other_review(database_with_data: Session, test_client: TestClient, auth_coach2: str): """Tests that a coach can't update other coaches their suggestions""" - form = { - "username": "coach2@noutlook.be", - "password": "wachtwoord" - } - token = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token - assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth}, json={ + + assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth_coach2}, json={ "suggestion": 3, "argumentation": "test"}).status_code == status.HTTP_403_FORBIDDEN suggestion: Suggestion = database_with_data.query( Suggestion).where(Suggestion.suggestion_id == 1).one() From 26a0c621ed8df6e0e21aaaded7819cc9f919c50b Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Mon, 21 Mar 2022 15:16:22 +0100 Subject: [PATCH 071/536] fix typo --- .../test_students/test_suggestions/test_suggestions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ff48eaf0b..1f012a578 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 @@ -241,7 +241,7 @@ def test_update_suggestion_admin(database_with_data: Session, test_client: TestC assert suggestion.argumentation == "test" -def test_update_suggestion_coach_their_review(database_with_data: Session, test_client: TestClient, auth_coac1: str): +def test_update_suggestion_coach_their_review(database_with_data: Session, test_client: TestClient, auth_coach1: str): """Tests that a coach can update their own suggestion""" assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth_coach1}, json={ From a183352c5f074ba72a14ddd1bc598437e5265075 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Mon, 21 Mar 2022 15:40:28 +0100 Subject: [PATCH 072/536] CRUD definitive decision student --- backend/src/database/crud/students.py | 11 ++++++-- .../test_database/test_crud/test_students.py | 25 ++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/backend/src/database/crud/students.py b/backend/src/database/crud/students.py index 3b10af6a2..f33e77e16 100644 --- a/backend/src/database/crud/students.py +++ b/backend/src/database/crud/students.py @@ -1,5 +1,12 @@ from sqlalchemy.orm import Session +from src.database.enums import DecisionEnum from src.database.models import Student -def get_student_by_id(database_session: Session, student_id: int) -> Student: - return database_session.query(Student).where(Student.student_id == student_id).one() + +def get_student_by_id(db: Session, student_id: int) -> Student: + return db.query(Student).where(Student.student_id == student_id).one() + + +def set_definitive_decision_on_student(db: Session, student: Student, decision: DecisionEnum) -> None: + student.decision = decision + db.commit() diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index a1c1fb434..9f7bc016e 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -2,7 +2,8 @@ from sqlalchemy.orm import Session from sqlalchemy.orm.exc import NoResultFound from src.database.models import Student, User, Edition, Skill -from src.database.crud.students import get_student_by_id +from src.database.enums import DecisionEnum +from src.database.crud.students import get_student_by_id, set_definitive_decision_on_student @pytest.fixture @@ -53,6 +54,7 @@ def database_with_data(database_session: Session): return database_session + def test_get_student_by_id(database_with_data: Session): """Tests if you get the right student""" student: Student = get_student_by_id(database_with_data, 1) @@ -66,3 +68,24 @@ def test_no_student(database_with_data: Session): """Tests if you get an error for a not existing student""" with pytest.raises(NoResultFound): get_student_by_id(database_with_data, 5) + + +def test_definitive_decision_on_student_YES(database_with_data: Session): + student: Student = get_student_by_id(database_with_data, 1) + 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): + student: Student = get_student_by_id(database_with_data, 1) + 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): + student: Student = get_student_by_id(database_with_data, 1) + set_definitive_decision_on_student( + database_with_data, student, DecisionEnum.NO) + assert student.decision == DecisionEnum.NO From a1fd8f6ee619b2c99f0f1800a475a58d923f47cf Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Mon, 21 Mar 2022 16:56:00 +0100 Subject: [PATCH 073/536] closes #106 --- backend/src/app/logic/students.py | 10 ++ .../app/routers/editions/students/students.py | 29 ++-- backend/src/app/schemas/students.py | 6 + backend/src/database/crud/students.py | 4 + .../test_students/test_students.py | 150 ++++++++++++++++++ 5 files changed, 189 insertions(+), 10 deletions(-) create mode 100644 backend/src/app/logic/students.py create mode 100644 backend/src/app/schemas/students.py create mode 100644 backend/tests/test_routers/test_editions/test_students/test_students.py diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py new file mode 100644 index 000000000..7e3bb631b --- /dev/null +++ b/backend/src/app/logic/students.py @@ -0,0 +1,10 @@ +from sqlalchemy.orm import Session + +from src.app.schemas.students import NewDecision +from src.database.crud.students import set_definitive_decision_on_student +from src.database.models import Suggestion, User, Student +#from src.app.schemas.suggestion import SuggestionListResponse, SuggestionResponse +from src.app.exceptions.authentication import MissingPermissionsException + +def definitive_decision_on_student(db: Session, student: Student, decision: NewDecision): + set_definitive_decision_on_student(db, student, decision.decision) \ No newline at end of file diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index 77d71cf0b..812193217 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -1,15 +1,18 @@ from fastapi import APIRouter, Depends from sqlalchemy.orm import Session +from starlette import status from src.app.routers.tags import Tags -from src.app.utils.dependencies import get_edition +from src.app.utils.dependencies import get_student, get_edition, require_admin +from src.app.logic.students import definitive_decision_on_student from src.database.database import get_session -from src.database.models import Edition - +from src.database.models import Student, Edition from .suggestions import students_suggestions_router +from src.app.schemas.students import NewDecision students_router = APIRouter(prefix="/students", tags=[Tags.STUDENTS]) -students_router.include_router(students_suggestions_router, prefix="/{student_id}") +students_router.include_router( + students_suggestions_router, prefix="/{student_id}") @students_router.get("/") @@ -17,41 +20,47 @@ async def get_students(db: Session = Depends(get_session), edition: Edition = De """ Get a list of all students. """ + pass @students_router.post("/emails") -async def send_emails(edition_id: int): +async def send_emails(edition: Edition = Depends(get_edition)): """ Send a Yes/Maybe/No email to a list of students. """ + pass @students_router.delete("/{student_id}") -async def delete_student(edition_id: int, student_id: int): +async def delete_student(edition: Edition = Depends(get_edition), student: Student = Depends(get_student)): """ Delete all information stored about a specific student. """ + pass @students_router.get("/{student_id}") -async def get_student(edition_id: int, student_id: int): +async def get_student_by_id(edition: Edition = Depends(get_edition), student: Student = Depends(get_student)): """ Get information about a specific student. """ + pass -@students_router.post("/{student_id}/decision") -async def make_decision(edition_id: int, student_id: int): +@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)) -> None: """ 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") -async def get_student_email_history(edition_id: int, student_id: int): +async def get_student_email_history(edition: Edition = Depends(get_edition), student: Student = Depends(get_student)): """ Get the history of all Yes/Maybe/No emails that have been sent to a specific student so far. """ + pass diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py new file mode 100644 index 000000000..6965f4221 --- /dev/null +++ b/backend/src/app/schemas/students.py @@ -0,0 +1,6 @@ +from src.app.schemas.webhooks import CamelCaseModel +from src.database.enums import DecisionEnum + +class NewDecision(CamelCaseModel): + """the fields of a decision""" + decision: DecisionEnum \ No newline at end of file diff --git a/backend/src/database/crud/students.py b/backend/src/database/crud/students.py index f33e77e16..3d5103960 100644 --- a/backend/src/database/crud/students.py +++ b/backend/src/database/crud/students.py @@ -4,9 +4,13 @@ def get_student_by_id(db: Session, student_id: int) -> Student: + """Get a student by id""" + return db.query(Student).where(Student.student_id == student_id).one() def set_definitive_decision_on_student(db: Session, student: Student, decision: DecisionEnum) -> None: + """set a definitive decision on a student""" + student.decision = decision db.commit() 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 new file mode 100644 index 000000000..62756a75e --- /dev/null +++ b/backend/tests/test_routers/test_editions/test_students/test_students.py @@ -0,0 +1,150 @@ +import pytest +from sqlalchemy.orm import Session +from starlette import status +from starlette.testclient import TestClient +from src.database.enums import DecisionEnum +from src.database.models import Student, User, Edition, Skill, AuthEmail +from src.app.logic.security import get_password_hash + + +@pytest.fixture +def database_with_data(database_session: Session) -> Session: + """A fixture to fill the database with fake data that can easly be used when testing""" + + # Editions + edition: Edition = Edition(year=2022) + database_session.add(edition) + + # Users + admin: User = User(name="admin", email="admin@ngmail.com", admin=True) + coach1: User = User(name="coach1", email="coach1@noutlook.be") + coach2: User = User(name="coach2", email="coach2@noutlook.be") + request: User = User(name="request", email="request@ngmail.com") + database_session.add(admin) + database_session.add(coach1) + database_session.add(coach2) + database_session.add(request) + + # AuthEmail + pw_hash = get_password_hash("wachtwoord") + auth_email_admin: AuthEmail = AuthEmail(user=admin, pw_hash=pw_hash) + auth_email_coach1: AuthEmail = AuthEmail(user=coach1, pw_hash=pw_hash) + auth_email_coach2: AuthEmail = AuthEmail(user=coach2, pw_hash=pw_hash) + auth_email_request: AuthEmail = AuthEmail(user=request, pw_hash=pw_hash) + database_session.add(auth_email_admin) + database_session.add(auth_email_coach1) + database_session.add(auth_email_coach2) + database_session.add(auth_email_request) + + # 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") + database_session.add(skill1) + database_session.add(skill2) + database_session.add(skill3) + database_session.add(skill4) + database_session.add(skill5) + database_session.add(skill6) + + # Student + 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, 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=True, + wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill4, skill5]) + + database_session.add(student01) + database_session.add(student30) + + database_session.commit() + return database_session + + +@pytest.fixture +def auth_coach1(test_client: TestClient) -> str: + """A fixture for logging in coach1""" + + form = { + "username": "coach1@noutlook.be", + "password": "wachtwoord" + } + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + return auth + + +@pytest.fixture +def auth_coach2(test_client: TestClient) -> str: + """A fixture for logging in coach1""" + + form = { + "username": "coach2@noutlook.be", + "password": "wachtwoord" + } + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + return auth + + +@pytest.fixture +def auth_admin(test_client: TestClient) -> str: + """A fixture for logging in admin""" + + form = { + "username": "admin@ngmail.com", + "password": "wachtwoord" + } + token = test_client.post("/login/token", data=form).json()["accessToken"] + auth = "Bearer " + token + return auth + + +def test_set_definitive_decision_no_authorization(database_with_data: Session, test_client: TestClient): + """tests""" + assert test_client.put("/editions/1/students/2/decision", headers={ + "Authorization": "auth"}).status_code == status.HTTP_401_UNAUTHORIZED + + +def test_set_definitive_decision_coach(database_with_data: Session, test_client: TestClient, auth_coach1): + """tests""" + assert test_client.put("/editions/1/students/2/decision", headers={ + "Authorization": auth_coach1}).status_code == status.HTTP_403_FORBIDDEN + + +def test_set_definitive_decision_on_ghost(database_with_data: Session, test_client: TestClient, auth_admin: str): + """tests""" + assert test_client.put("/editions/1/students/100/decision", + headers={"Authorization": auth_admin}).status_code == status.HTTP_404_NOT_FOUND + + +def test_set_definitive_decision_wrong_body(database_with_data: Session, test_client: TestClient, auth_admin: str): + """tests""" + assert test_client.put("/editions/1/students/1/decision", + headers={"Authorization": auth_admin}).status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + +def test_set_definitive_decision_YES(database_with_data: Session, test_client: TestClient, auth_admin: str): + """tests""" + assert test_client.put("/editions/1/students/1/decision", + headers={"Authorization": auth_admin}, 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 + +def test_set_definitive_decision_NO(database_with_data: Session, test_client: TestClient, auth_admin: str): + """tests""" + assert test_client.put("/editions/1/students/1/decision", + headers={"Authorization": auth_admin}, 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 + +def test_set_definitive_decision_MAYBE(database_with_data: Session, test_client: TestClient, auth_admin: str): + """tests""" + assert test_client.put("/editions/1/students/1/decision", + headers={"Authorization": auth_admin}, 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 \ No newline at end of file From f12d2617306e98b2597d9534b9da4291b415c6fa Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Mon, 21 Mar 2022 17:22:52 +0100 Subject: [PATCH 074/536] closes #103 --- backend/src/app/logic/students.py | 13 ++-- .../app/routers/editions/students/students.py | 14 ++--- backend/src/database/crud/students.py | 6 ++ .../test_database/test_crud/test_students.py | 17 +++-- .../test_students/test_students.py | 62 ++++++++++++++----- 5 files changed, 81 insertions(+), 31 deletions(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index 7e3bb631b..af0a19236 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -1,10 +1,15 @@ from sqlalchemy.orm import Session from src.app.schemas.students import NewDecision -from src.database.crud.students import set_definitive_decision_on_student -from src.database.models import Suggestion, User, Student +from src.database.crud.students import set_definitive_decision_on_student, delete_student +from src.database.models import Student #from src.app.schemas.suggestion import SuggestionListResponse, SuggestionResponse -from src.app.exceptions.authentication import MissingPermissionsException def definitive_decision_on_student(db: Session, student: Student, decision: NewDecision): - set_definitive_decision_on_student(db, student, decision.decision) \ No newline at end of file + """Set a definitive decion on a student""" + set_definitive_decision_on_student(db, student, decision.decision) + + +def remove_student(db: Session, student: Student): + """delete a student""" + delete_student(db, student) diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index 812193217..c0b2252d1 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -4,11 +4,11 @@ from starlette import status from src.app.routers.tags import Tags from src.app.utils.dependencies import get_student, get_edition, require_admin -from src.app.logic.students import definitive_decision_on_student +from src.app.logic.students import definitive_decision_on_student, remove_student +from src.app.schemas.students import NewDecision from src.database.database import get_session from src.database.models import Student, Edition from .suggestions import students_suggestions_router -from src.app.schemas.students import NewDecision students_router = APIRouter(prefix="/students", tags=[Tags.STUDENTS]) students_router.include_router( @@ -20,7 +20,6 @@ async def get_students(db: Session = Depends(get_session), edition: Edition = De """ Get a list of all students. """ - pass @students_router.post("/emails") @@ -28,15 +27,14 @@ async def send_emails(edition: Edition = Depends(get_edition)): """ Send a Yes/Maybe/No email to a list of students. """ - pass -@students_router.delete("/{student_id}") -async def delete_student(edition: Edition = Depends(get_edition), student: Student = Depends(get_student)): +@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)) -> None: """ Delete all information stored about a specific student. """ - pass + remove_student(db,student) @students_router.get("/{student_id}") @@ -44,7 +42,6 @@ async def get_student_by_id(edition: Edition = Depends(get_edition), student: St """ Get information about a specific student. """ - pass @students_router.put("/{student_id}/decision", dependencies=[Depends(require_admin)], status_code=status.HTTP_204_NO_CONTENT) @@ -63,4 +60,3 @@ async def get_student_email_history(edition: Edition = Depends(get_edition), stu Get the history of all Yes/Maybe/No emails that have been sent to a specific student so far. """ - pass diff --git a/backend/src/database/crud/students.py b/backend/src/database/crud/students.py index 3d5103960..cc203e526 100644 --- a/backend/src/database/crud/students.py +++ b/backend/src/database/crud/students.py @@ -14,3 +14,9 @@ def set_definitive_decision_on_student(db: Session, student: Student, decision: student.decision = decision db.commit() + + +def delete_student(db: Session, student: Student) -> None: + """Delete a student from the database""" + db.delete(student) + db.commit() diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index 9f7bc016e..83bde4d57 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -3,7 +3,7 @@ from sqlalchemy.orm.exc import NoResultFound from src.database.models import Student, User, Edition, Skill from src.database.enums import DecisionEnum -from src.database.crud.students import get_student_by_id, set_definitive_decision_on_student +from src.database.crud.students import get_student_by_id, set_definitive_decision_on_student, delete_student @pytest.fixture @@ -70,22 +70,31 @@ def test_no_student(database_with_data: Session): get_student_by_id(database_with_data, 5) -def test_definitive_decision_on_student_YES(database_with_data: Session): +def test_definitive_decision_on_student_yes(database_with_data: Session): + """Tests for definitive decision yes""" student: Student = get_student_by_id(database_with_data, 1) 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): +def test_definitive_decision_on_student_maybe(database_with_data: Session): + """Tests for definitive decision maybe""" student: Student = get_student_by_id(database_with_data, 1) 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): +def test_definitive_decision_on_student_no(database_with_data: Session): + """Tests for definitive decision no""" student: Student = get_student_by_id(database_with_data, 1) set_definitive_decision_on_student( database_with_data, student, DecisionEnum.NO) assert student.decision == DecisionEnum.NO + + +def test_delete_student(database_with_data: Session): + """Tests for deleting a student""" + student: Student = get_student_by_id(database_with_data, 1) + delete_student \ No newline at end of file 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 62756a75e..050d1bb9e 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 @@ -107,44 +107,78 @@ def auth_admin(test_client: TestClient) -> str: def test_set_definitive_decision_no_authorization(database_with_data: Session, test_client: TestClient): """tests""" assert test_client.put("/editions/1/students/2/decision", headers={ - "Authorization": "auth"}).status_code == status.HTTP_401_UNAUTHORIZED + "Authorization": "auth"}).status_code == status.HTTP_401_UNAUTHORIZED def test_set_definitive_decision_coach(database_with_data: Session, test_client: TestClient, auth_coach1): """tests""" assert test_client.put("/editions/1/students/2/decision", headers={ - "Authorization": auth_coach1}).status_code == status.HTTP_403_FORBIDDEN + "Authorization": auth_coach1}).status_code == status.HTTP_403_FORBIDDEN def test_set_definitive_decision_on_ghost(database_with_data: Session, test_client: TestClient, auth_admin: str): """tests""" assert test_client.put("/editions/1/students/100/decision", - headers={"Authorization": auth_admin}).status_code == status.HTTP_404_NOT_FOUND + headers={"Authorization": auth_admin}).status_code == status.HTTP_404_NOT_FOUND def test_set_definitive_decision_wrong_body(database_with_data: Session, test_client: TestClient, auth_admin: str): """tests""" assert test_client.put("/editions/1/students/1/decision", - headers={"Authorization": auth_admin}).status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + headers={"Authorization": auth_admin}).status_code == status.HTTP_422_UNPROCESSABLE_ENTITY -def test_set_definitive_decision_YES(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_set_definitive_decision_yes(database_with_data: Session, test_client: TestClient, auth_admin: str): """tests""" assert test_client.put("/editions/1/students/1/decision", - headers={"Authorization": auth_admin}, json={"decision": 1}).status_code == status.HTTP_204_NO_CONTENT - student: Student = database_with_data.query(Student).where(Student.student_id == 1).one() + headers={"Authorization": auth_admin}, + 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 -def test_set_definitive_decision_NO(database_with_data: Session, test_client: TestClient, auth_admin: str): + +def test_set_definitive_decision_no(database_with_data: Session, test_client: TestClient, auth_admin: str): """tests""" assert test_client.put("/editions/1/students/1/decision", - headers={"Authorization": auth_admin}, json={"decision": 3}).status_code == status.HTTP_204_NO_CONTENT - student: Student = database_with_data.query(Student).where(Student.student_id == 1).one() + headers={"Authorization": auth_admin}, + 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 -def test_set_definitive_decision_MAYBE(database_with_data: Session, test_client: TestClient, auth_admin: str): + +def test_set_definitive_decision_maybe(database_with_data: Session, test_client: TestClient, auth_admin: str): """tests""" assert test_client.put("/editions/1/students/1/decision", - headers={"Authorization": auth_admin}, 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 \ No newline at end of file + headers={"Authorization": auth_admin}, + 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 + + +def test_delete_student_no_authorization(database_with_data: Session, test_client: TestClient): + """tests""" + assert test_client.delete("/editions/1/students/2", headers={ + "Authorization": "auth"}).status_code == status.HTTP_401_UNAUTHORIZED + + +def test_delete_student_coach(database_with_data: Session, test_client: TestClient, auth_coach1): + """tests""" + assert test_client.delete("/editions/1/students/2", headers={ + "Authorization": auth_coach1}).status_code == status.HTTP_403_FORBIDDEN + + +def test_delete_ghost(database_with_data: Session, test_client: TestClient, auth_admin: str): + """tests""" + assert test_client.delete("/editions/1/students/100", + headers={"Authorization": auth_admin}).status_code == status.HTTP_404_NOT_FOUND + + +def test_delete(database_with_data: Session, test_client: TestClient, auth_admin: str): + """tests""" + assert test_client.delete("/editions/1/students/1", + headers={"Authorization": auth_admin}).status_code == status.HTTP_204_NO_CONTENT + students: Student = database_with_data.query(Student).where(Student.student_id == 1).all() + assert len(students) == 0 From 6e228e6d8ccb4c0c6a130f90de476bb805ce8dd4 Mon Sep 17 00:00:00 2001 From: beguille Date: Tue, 22 Mar 2022 19:55:50 +0100 Subject: [PATCH 075/536] newly created project is now returned --- backend/src/app/logic/projects.py | 9 ++++++--- backend/src/app/routers/editions/projects/projects.py | 4 ++-- backend/src/app/schemas/projects.py | 5 ----- backend/src/database/crud/projects.py | 6 +++--- .../test_editions/test_projects/test_projects.py | 1 + 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index f2ec4366b..1d9bbeac4 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session -from src.app.schemas.projects import ProjectList, Project, ConflictStudentList, ProjectId +from src.app.schemas.projects import ProjectList, Project, ConflictStudentList from src.database.crud.projects import db_get_all_projects, db_add_project, db_delete_project, \ db_patch_project, db_get_conflict_students from src.database.models import Edition @@ -13,9 +13,12 @@ def logic_get_project_list(db: Session, edition: Edition) -> ProjectList: def logic_create_project(db: Session, edition: Edition, name: str, number_of_students: int, skills: list[int], - partners: list[str], coaches: list[int]) -> ProjectId: + partners: list[str], coaches: list[int]) -> Project: """Create a new project""" - return db_add_project(db, edition, name, number_of_students, skills, partners, coaches) + project = db_add_project(db, edition, name, number_of_students, skills, partners, coaches) + return Project(project_id=project.project_id, name=project.name, number_of_students=project.number_of_students, + edition_id=project.edition_id, coaches=project.coaches, skills=project.skills, + partners=project.partners, project_roles=project.project_roles) def logic_delete_project(db: Session, project_id: int): diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index 387596f5a..d8bdf6406 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -7,7 +7,7 @@ logic_patch_project, logic_get_conflicts from src.app.routers.tags import Tags from src.app.schemas.projects import ProjectList, Project, InputProject, \ - ConflictStudentList, ProjectId + ConflictStudentList from src.app.utils.dependencies import get_edition, get_project from src.database.database import get_session from src.database.models import Edition @@ -25,7 +25,7 @@ async def get_projects(db: Session = Depends(get_session), edition: Edition = De return logic_get_project_list(db, edition) -@projects_router.post("/", status_code=status.HTTP_201_CREATED, response_model=ProjectId) +@projects_router.post("/", status_code=status.HTTP_201_CREATED, response_model=Project) async def create_project(input_project: InputProject, db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): """ diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index 9a3459f29..42c6a3362 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -95,11 +95,6 @@ class ConflictStudentList(CamelCaseModel): conflict_students: list[ConflictStudent] -class ProjectId(CamelCaseModel): - """Used for returning the id of a project after creating it""" - project_id: int - - class InputProject(BaseModel): """Used for passing the details of a project when creating/patching a project""" name: str diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index f6654330d..d4e39df0e 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -1,7 +1,7 @@ from sqlalchemy.exc import NoResultFound from sqlalchemy.orm import Session -from src.app.schemas.projects import ConflictStudent, ProjectId +from src.app.schemas.projects import ConflictStudent from src.database.models import Project, Edition, Student, ProjectRole, Skill, User, Partner @@ -11,7 +11,7 @@ def db_get_all_projects(db: Session, edition: Edition) -> list[Project]: def db_add_project(db: Session, edition: Edition, name: str, number_of_students: int, skills: list[int], - partners: list[str], coaches: list[int]) -> ProjectId: + partners: list[str], coaches: list[int]) -> Project: """ Add a project to the database If there are partner names that are not already in the database, add them @@ -31,7 +31,7 @@ def db_add_project(db: Session, edition: Edition, name: str, number_of_students: db.add(project) db.commit() - return ProjectId(project_id=project.project_id) + return project def db_get_project(db: Session, project_id: int) -> Project: 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 77fa2f29e..53b650478 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 @@ -66,6 +66,7 @@ def test_create_project(database_session: Session, test_client: TestClient): assert response.status_code == status.HTTP_201_CREATED assert response.json()['projectId'] == 1 + assert response.json()['name'] == 'test' response2 = test_client.get('/editions/1/projects') json = response2.json() From c5ef8d4ad41af259592d1151edf1649389d7598c Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Wed, 23 Mar 2022 21:58:45 +0100 Subject: [PATCH 076/536] tests project crud + bug fix + type fault fix --- backend/src/app/logic/projects.py | 2 +- backend/src/database/crud/projects.py | 6 +- .../test_database/test_crud/test_projects.py | 182 ++++++++++++++++++ .../test_crud/test_projects_students.py | 3 + 4 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 backend/tests/test_database/test_crud/test_projects.py create mode 100644 backend/tests/test_database/test_crud/test_projects_students.py diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index 1d9bbeac4..7ad92f373 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -26,7 +26,7 @@ def logic_delete_project(db: Session, project_id: int): db_delete_project(db, project_id) -def logic_patch_project(db: Session, project: Project, name: str, number_of_students: int, skills: [int], +def logic_patch_project(db: Session, project: Project, name: str, number_of_students: int, skills: list[int], partners: list[str], coaches: list[int]): """Make changes to a project""" db_patch_project(db, project, name, number_of_students, skills, partners, coaches) diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index d4e39df0e..fdb410054 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -17,7 +17,7 @@ def db_add_project(db: Session, edition: Edition, name: str, number_of_students: 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 skills] - coaches_obj = [db.query(User).where(User.user_id) == coach for coach in coaches] + coaches_obj = [db.query(User).where(User.user_id == coach).one() for coach in coaches] partners_obj = [] for partner in partners: try: @@ -41,6 +41,8 @@ def db_get_project(db: Session, project_id: int) -> Project: def db_delete_project(db: Session, project_id: int): """Delete a specific project from the database""" + # TODO: Maybe make the relationship between project and project_role cascade on delete? + # so this code is handled by the database proj_roles = db.query(ProjectRole).where(ProjectRole.project_id == project_id).all() for proj_role in proj_roles: db.delete(proj_role) @@ -57,7 +59,7 @@ def db_patch_project(db: Session, project: Project, name: str, number_of_student 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 skills] - coaches_obj = [db.query(User).where(User.user_id) == coach for coach in coaches] + coaches_obj = [db.query(User).where(User.user_id == coach).one() for coach in coaches] partners_obj = [] for partner in partners: try: diff --git a/backend/tests/test_database/test_crud/test_projects.py b/backend/tests/test_database/test_crud/test_projects.py new file mode 100644 index 000000000..a7adedc25 --- /dev/null +++ b/backend/tests/test_database/test_crud/test_projects.py @@ -0,0 +1,182 @@ +from calendar import day_abbr +from click import edit +import pytest +from sqlalchemy.orm import Session +from sqlalchemy.exc import NoResultFound + +from src.app.schemas.projects import ConflictStudent +from src.database.crud.projects import (db_get_all_projects, db_add_project, + db_get_project, db_delete_project, + db_patch_project, db_get_conflict_students) +from src.database.models import Edition, Partner, Project, User, Skill, ProjectRole, Student + + +@pytest.fixture +def database_with_data(database_session: Session) -> Session: + """fixture for adding data to the database""" + edition: Edition = Edition(year=2022) + 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) + user: User = User(name="coach1", email="user@user.be") + 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_all_projects_empty(database_session: Session): + """test get all projects but there are none""" + edition: Edition = Edition(year=2022) + database_session.add(edition) + database_session.commit() + projects: list[Project] = db_get_all_projects( + database_session, edition) + assert len(projects) == 0 + + +def test_get_all_projects(database_with_data: Session, current_edition: Edition): + """test get all projects""" + projects: list[Project] = db_get_all_projects( + database_with_data, current_edition) + assert len(projects) == 3 + + +def test_add_project_partner_do_not_exist_yet(database_with_data: Session, current_edition: Edition): + """tests add a project when the project don't exist yet""" + assert len(database_with_data.query(Partner).where( + Partner.name == "ugent").all()) == 0 + new_project: Project = db_add_project( + database_with_data, current_edition, "project1", 2, [1, 3], ["ugent"], [1]) + 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 """ + database_with_data.add(Partner(name="ugent")) + assert len(database_with_data.query(Partner).where( + Partner.name == "ugent").all()) == 1 + new_project: Project = db_add_project( + database_with_data, current_edition, "project1", 2, [1, 3], ["ugent"], [1]) + 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): + """test project that don't exist""" + with pytest.raises(NoResultFound): + db_get_project(database_with_data, 500) + + +def test_get_project(database_with_data: Session): + """test get project""" + project: Project = db_get_project(database_with_data, 1) + assert project.name == "project1" + assert project.number_of_students == 2 + + +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(db_get_all_projects(database_with_data, current_edition)) == 3 + db_delete_project(database_with_data, 3) + assert len(db_get_all_projects(database_with_data, current_edition)) == 2 + assert 3 not in [project.project_id for project in db_get_all_projects( + database_with_data, current_edition)] + + +def test_delete_project_with_project_roles(database_with_data: Session, current_edition): + """test delete a project that has project roles""" + assert len(database_with_data.query(ProjectRole).where( + ProjectRole.project_id == 1).all()) > 0 + assert len(db_get_all_projects(database_with_data, current_edition)) == 3 + db_delete_project(database_with_data, 1) + assert len(db_get_all_projects(database_with_data, current_edition)) == 2 + assert 1 not in [project.project_id for project in db_get_all_projects( + 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): + """tests patch a project""" + assert len(database_with_data.query(Partner).where( + Partner.name == "ugent").all()) == 0 + new_project: Project = db_add_project( + database_with_data, current_edition, "projec1", 2, [1, 3], ["ugent"], [1]) + 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() + db_patch_project(database_with_data, new_project, + "project1", 2, [1, 3], ["ugent"], [1]) + + assert new_partner in new_project.partners + assert new_project.name == "project1" + + +def test_get_conflict_students(database_with_data: Session, current_edition: Edition): + """test if the right ConflictStudent is given""" + conflicts: list[ConflictStudent] = db_get_conflict_students(database_with_data, current_edition) + assert len(conflicts) == 1 + assert conflicts[0].student.student_id == 1 + assert len(conflicts[0].projects) == 2 + assert conflicts[0].projects[0].project_id == 1 + assert conflicts[0].projects[1].project_id == 2 \ No newline at end of file diff --git a/backend/tests/test_database/test_crud/test_projects_students.py b/backend/tests/test_database/test_crud/test_projects_students.py new file mode 100644 index 000000000..7970a28f7 --- /dev/null +++ b/backend/tests/test_database/test_crud/test_projects_students.py @@ -0,0 +1,3 @@ +import pytest +import sqlalchemy.exc +from sqlalchemy.orm import Session \ No newline at end of file From 772f22f78ac380e7335e7fac5e47593dccafaaa7 Mon Sep 17 00:00:00 2001 From: Ward Meersman <48222993+WardM99@users.noreply.github.com> Date: Wed, 23 Mar 2022 22:06:04 +0100 Subject: [PATCH 077/536] Update backend/tests/test_routers/test_editions/test_projects/test_projects.py --- .../test_routers/test_editions/test_projects/test_projects.py | 1 - 1 file changed, 1 deletion(-) 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 53b650478..6b9dff44f 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,4 +1,3 @@ -from json import dumps import pytest from fastapi.testclient import TestClient From 564bb9239850e19bb8142caadcb167414066f373 Mon Sep 17 00:00:00 2001 From: Ward Meersman <48222993+WardM99@users.noreply.github.com> Date: Wed, 23 Mar 2022 22:06:08 +0100 Subject: [PATCH 078/536] Update backend/tests/test_routers/test_editions/test_projects/test_projects.py --- .../test_routers/test_editions/test_projects/test_projects.py | 1 - 1 file changed, 1 deletion(-) 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 6b9dff44f..2d8cea311 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,5 +1,4 @@ -import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session from starlette import status From a087976e3ab3ed2d021c5a55ef06412507348343 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Wed, 23 Mar 2022 23:03:04 +0100 Subject: [PATCH 079/536] pylint fixes + test crud/project_roles --- .../src/database/crud/projects_students.py | 8 +- .../test_database/test_crud/test_projects.py | 2 - .../test_crud/test_projects_students.py | 97 ++++++++++++++++++- 3 files changed, 100 insertions(+), 7 deletions(-) diff --git a/backend/src/database/crud/projects_students.py b/backend/src/database/crud/projects_students.py index 7830488ed..c6d2bd65c 100644 --- a/backend/src/database/crud/projects_students.py +++ b/backend/src/database/crud/projects_students.py @@ -5,8 +5,9 @@ def db_remove_student_project(db: Session, project: Project, student_id: int): """Remove a student from a project in the database""" - pr = db.query(ProjectRole).where(ProjectRole.student_id == student_id and ProjectRole.project == project).one() - db.delete(pr) + proj_role = db.query(ProjectRole).where(ProjectRole.student_id == + student_id).where(ProjectRole.project == project).one() + db.delete(proj_role) db.commit() @@ -20,7 +21,8 @@ def db_add_student_project(db: Session, project: Project, student_id: int, skill def db_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""" - proj_role = db.query(ProjectRole).where(ProjectRole.student_id == student_id and ProjectRole.project == project).one() + 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() diff --git a/backend/tests/test_database/test_crud/test_projects.py b/backend/tests/test_database/test_crud/test_projects.py index a7adedc25..41d6f2068 100644 --- a/backend/tests/test_database/test_crud/test_projects.py +++ b/backend/tests/test_database/test_crud/test_projects.py @@ -1,5 +1,3 @@ -from calendar import day_abbr -from click import edit import pytest from sqlalchemy.orm import Session from sqlalchemy.exc import NoResultFound 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 7970a28f7..44fa96a05 100644 --- a/backend/tests/test_database/test_crud/test_projects_students.py +++ b/backend/tests/test_database/test_crud/test_projects_students.py @@ -1,3 +1,96 @@ import pytest -import sqlalchemy.exc -from sqlalchemy.orm import Session \ No newline at end of file +from sqlalchemy.orm import Session +from sqlalchemy.exc import NoResultFound + +from src.database.crud.projects_students import ( + db_remove_student_project, db_add_student_project, db_change_project_role) +from src.database.models import Edition, Project, User, Skill, ProjectRole, Student + + +@pytest.fixture +def database_with_data(database_session: Session) -> Session: + """fixture for adding data to the database""" + edition: Edition = Edition(year=2022) + 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) + user: User = User(name="coach1", email="user@user.be") + 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() + db_remove_student_project(database_with_data, project, 1) + assert len(database_with_data.query(ProjectRole).where( + ProjectRole.student_id == 1).all()) == 1 + + +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): + db_remove_student_project(database_with_data, project, 2) + + +def test_add_student_project(database_with_data: Session): + """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() + db_add_student_project(database_with_data, project, 2, 2, 1) + assert len(database_with_data.query(ProjectRole).where( + ProjectRole.student_id == 2).all()) == 2 + + +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 + db_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): + db_change_project_role(database_with_data, project, 2, 2, 1) \ No newline at end of file From c998d2d50a9d7b351128c8a2eaf9764f11b773e8 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 26 Mar 2022 12:18:48 +0100 Subject: [PATCH 080/536] refactor navbar files --- frontend/src/App.tsx | 2 +- frontend/src/components/navbar/{index.tsx => NavBar.tsx} | 1 + frontend/src/components/navbar/index.ts | 1 + frontend/src/components/navbar/navbar.css | 7 +++++++ frontend/src/css-files/App.css | 6 ------ 5 files changed, 10 insertions(+), 7 deletions(-) rename frontend/src/components/navbar/{index.tsx => NavBar.tsx} (97%) create mode 100644 frontend/src/components/navbar/index.ts create mode 100644 frontend/src/components/navbar/navbar.css diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a4ce546db..334ac61aa 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import "bootstrap/dist/css/bootstrap.min.css"; import "./css-files/App.css"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; -import NavBar from "./components/navbar"; +import NavBar from "./components/NavBar"; import LoginPage from "./views/LoginPage/LoginPage"; import Students from "./views/Students"; import Users from "./views/Users"; diff --git a/frontend/src/components/navbar/index.tsx b/frontend/src/components/navbar/NavBar.tsx similarity index 97% rename from frontend/src/components/navbar/index.tsx rename to frontend/src/components/navbar/NavBar.tsx index 0ff41f5e5..f7a99db69 100644 --- a/frontend/src/components/navbar/index.tsx +++ b/frontend/src/components/navbar/NavBar.tsx @@ -1,5 +1,6 @@ import React from "react"; import { Nav, NavLink, Bars, NavMenu } from "./NavBarElementss"; +import "./navbar.css"; function NavBar({ token }: any, {setToken}: any) { let hidden = "nav-hidden"; diff --git a/frontend/src/components/navbar/index.ts b/frontend/src/components/navbar/index.ts new file mode 100644 index 000000000..79cca0c6f --- /dev/null +++ b/frontend/src/components/navbar/index.ts @@ -0,0 +1 @@ +export { default } from "./NavBar" \ No newline at end of file diff --git a/frontend/src/components/navbar/navbar.css b/frontend/src/components/navbar/navbar.css new file mode 100644 index 000000000..f3170af8d --- /dev/null +++ b/frontend/src/components/navbar/navbar.css @@ -0,0 +1,7 @@ +.nav-links { + display: flex; +} + +.nav-hidden { + visibility: hidden; +} \ No newline at end of file diff --git a/frontend/src/css-files/App.css b/frontend/src/css-files/App.css index ee150fcac..9a1d6988f 100644 --- a/frontend/src/css-files/App.css +++ b/frontend/src/css-files/App.css @@ -12,13 +12,7 @@ height: 100%; } -.nav-links { - display: flex; -} -.nav-hidden { - visibility: hidden; -} * { box-sizing: border-box; From 8d976dc101815ba1a924335059b0cfae16d6b928 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 26 Mar 2022 12:35:07 +0100 Subject: [PATCH 081/536] refactor error page --- frontend/src/views/ErrorPage.tsx | 7 ------- frontend/src/views/ErrorPage/ErrorPage.css | 6 ++++++ frontend/src/views/ErrorPage/ErrorPage.tsx | 12 ++++++++++++ 3 files changed, 18 insertions(+), 7 deletions(-) delete mode 100644 frontend/src/views/ErrorPage.tsx create mode 100644 frontend/src/views/ErrorPage/ErrorPage.css create mode 100644 frontend/src/views/ErrorPage/ErrorPage.tsx diff --git a/frontend/src/views/ErrorPage.tsx b/frontend/src/views/ErrorPage.tsx deleted file mode 100644 index 126863917..000000000 --- a/frontend/src/views/ErrorPage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -function ErrorPage() { - return

404: Page not found

; -} - -export default ErrorPage; diff --git a/frontend/src/views/ErrorPage/ErrorPage.css b/frontend/src/views/ErrorPage/ErrorPage.css new file mode 100644 index 000000000..3dadd669f --- /dev/null +++ b/frontend/src/views/ErrorPage/ErrorPage.css @@ -0,0 +1,6 @@ +.error { + margin: auto; + margin-top: 10%; + max-width: 50%; + text-align: center; +} diff --git a/frontend/src/views/ErrorPage/ErrorPage.tsx b/frontend/src/views/ErrorPage/ErrorPage.tsx new file mode 100644 index 000000000..e82b5ad8a --- /dev/null +++ b/frontend/src/views/ErrorPage/ErrorPage.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import "./ErrorPage.css"; + +function ErrorPage() { + return ( +

+ Oops! This is awkward... You are looking for something that doesn't actually exists. +

+ ); +} + +export default ErrorPage; From 67b0ad60f2451a3dda83764d00d7c15f71d4f8c5 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 26 Mar 2022 14:03:46 +0100 Subject: [PATCH 082/536] every page has its directory and index file --- frontend/src/App.tsx | 14 +++--- frontend/src/css-files/LogInButtons.css | 48 ------------------- frontend/src/views/ErrorPage/index.ts | 1 + frontend/src/views/Home.tsx | 18 ------- frontend/src/views/LoginPage/index.ts | 1 + frontend/src/views/PendingPage/index.ts | 1 + .../src/views/ProjectsPage/ProjectsPage.css | 0 .../views/{ => ProjectsPage}/ProjectsPage.tsx | 1 + frontend/src/views/ProjectsPage/index.ts | 1 + .../src/views/RegisterPage/RegisterPage.css | 0 .../RegisterPage.tsx} | 5 +- frontend/src/views/RegisterPage/index.ts | 1 + .../src/views/StudentsPage/StudentsPage.css | 0 .../StudentsPage.tsx} | 1 + frontend/src/views/StudentsPage/index.ts | 1 + frontend/src/views/UsersPage/UsersPage.css | 0 .../{Users.tsx => UsersPage/UsersPage.tsx} | 0 frontend/src/views/UsersPage/index.ts | 1 + 18 files changed, 19 insertions(+), 75 deletions(-) delete mode 100644 frontend/src/css-files/LogInButtons.css create mode 100644 frontend/src/views/ErrorPage/index.ts delete mode 100644 frontend/src/views/Home.tsx create mode 100644 frontend/src/views/LoginPage/index.ts create mode 100644 frontend/src/views/PendingPage/index.ts create mode 100644 frontend/src/views/ProjectsPage/ProjectsPage.css rename frontend/src/views/{ => ProjectsPage}/ProjectsPage.tsx (92%) create mode 100644 frontend/src/views/ProjectsPage/index.ts create mode 100644 frontend/src/views/RegisterPage/RegisterPage.css rename frontend/src/views/{RegisterForm.tsx => RegisterPage/RegisterPage.tsx} (96%) create mode 100644 frontend/src/views/RegisterPage/index.ts create mode 100644 frontend/src/views/StudentsPage/StudentsPage.css rename frontend/src/views/{Students.tsx => StudentsPage/StudentsPage.tsx} (81%) create mode 100644 frontend/src/views/StudentsPage/index.ts create mode 100644 frontend/src/views/UsersPage/UsersPage.css rename frontend/src/views/{Users.tsx => UsersPage/UsersPage.tsx} (100%) create mode 100644 frontend/src/views/UsersPage/index.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 334ac61aa..e96ebae9e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,13 +3,13 @@ import "bootstrap/dist/css/bootstrap.min.css"; import "./css-files/App.css"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import NavBar from "./components/NavBar"; -import LoginPage from "./views/LoginPage/LoginPage"; -import Students from "./views/Students"; -import Users from "./views/Users"; -import ProjectsPage from "./views/ProjectsPage"; -import RegisterForm from "./views/RegisterForm"; -import ErrorPage from "./views/ErrorPage"; -import PendingPage from "./views/PendingPage/PendingPage"; +import LoginPage from "./views/LoginPage"; +import Students from "./views/StudentsPage/StudentsPage"; +import Users from "./views/UsersPage/UsersPage"; +import ProjectsPage from "./views/ProjectsPage/ProjectsPage"; +import RegisterForm from "./views/RegisterPage/RegisterPage"; +import ErrorPage from "./views/ErrorPage/ErrorPage"; +import PendingPage from "./views/PendingPage"; import Footer from "./components/Footer"; import { Container, ContentWrapper } from "./app.styles"; diff --git a/frontend/src/css-files/LogInButtons.css b/frontend/src/css-files/LogInButtons.css deleted file mode 100644 index b4bbf1fc4..000000000 --- a/frontend/src/css-files/LogInButtons.css +++ /dev/null @@ -1,48 +0,0 @@ -.login-buttons { - width: available; - margin-top: 60px; - display: flex; - justify-content: center; -} - -.login-button { - display: block; - position: relative; - width: 280px; - height: 40px; - font-size: 25px; - padding: 0.2rem; - background: white; - cursor: pointer; - color: var(--react_dark_grey); - border-radius: 10px; - border-color: black; - border-width: 2px; -} - -.login-button-content { - display: flex; - align-items: center; - margin-left: 10px; -} - -.email-login { - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 10px; -} - -.google-login { - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 10px; -} - -.github-login { - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 10px; -} diff --git a/frontend/src/views/ErrorPage/index.ts b/frontend/src/views/ErrorPage/index.ts new file mode 100644 index 000000000..4a1ddb1bf --- /dev/null +++ b/frontend/src/views/ErrorPage/index.ts @@ -0,0 +1 @@ +export { default } from "./ErrorPage"; diff --git a/frontend/src/views/Home.tsx b/frontend/src/views/Home.tsx deleted file mode 100644 index 7c770aa74..000000000 --- a/frontend/src/views/Home.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; - -function Home() { - return ( -
-

Home

-
- ); -} - -export default Home; diff --git a/frontend/src/views/LoginPage/index.ts b/frontend/src/views/LoginPage/index.ts new file mode 100644 index 000000000..f81523088 --- /dev/null +++ b/frontend/src/views/LoginPage/index.ts @@ -0,0 +1 @@ +export { default } from "./LoginPage"; diff --git a/frontend/src/views/PendingPage/index.ts b/frontend/src/views/PendingPage/index.ts new file mode 100644 index 000000000..72abd154f --- /dev/null +++ b/frontend/src/views/PendingPage/index.ts @@ -0,0 +1 @@ +export { default } from "./PendingPage"; diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.css b/frontend/src/views/ProjectsPage/ProjectsPage.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/views/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx similarity index 92% rename from frontend/src/views/ProjectsPage.tsx rename to frontend/src/views/ProjectsPage/ProjectsPage.tsx index a335327f1..ad2716db1 100644 --- a/frontend/src/views/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -1,4 +1,5 @@ import React from "react"; +import "./ProjectsPage.css"; function ProjectPage() { return ( diff --git a/frontend/src/views/ProjectsPage/index.ts b/frontend/src/views/ProjectsPage/index.ts new file mode 100644 index 000000000..7b601b450 --- /dev/null +++ b/frontend/src/views/ProjectsPage/index.ts @@ -0,0 +1 @@ +export { default } from "./ProjectsPage"; diff --git a/frontend/src/views/RegisterPage/RegisterPage.css b/frontend/src/views/RegisterPage/RegisterPage.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/views/RegisterForm.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx similarity index 96% rename from frontend/src/views/RegisterForm.tsx rename to frontend/src/views/RegisterPage/RegisterPage.tsx index efae8379f..719e50edb 100644 --- a/frontend/src/views/RegisterForm.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -1,7 +1,8 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { axiosInstance } from "../utils/api/api"; -import OSOCLetters from "../components/OSOCLetters/OSOCLetters"; +import { axiosInstance } from "../../utils/api/api"; +import OSOCLetters from "../../components/OSOCLetters/OSOCLetters"; +import "./RegisterPage.css"; import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; diff --git a/frontend/src/views/RegisterPage/index.ts b/frontend/src/views/RegisterPage/index.ts new file mode 100644 index 000000000..926657ec0 --- /dev/null +++ b/frontend/src/views/RegisterPage/index.ts @@ -0,0 +1 @@ +export { default } from "./RegisterPage"; diff --git a/frontend/src/views/StudentsPage/StudentsPage.css b/frontend/src/views/StudentsPage/StudentsPage.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/views/Students.tsx b/frontend/src/views/StudentsPage/StudentsPage.tsx similarity index 81% rename from frontend/src/views/Students.tsx rename to frontend/src/views/StudentsPage/StudentsPage.tsx index d1a6b4009..162122f17 100644 --- a/frontend/src/views/Students.tsx +++ b/frontend/src/views/StudentsPage/StudentsPage.tsx @@ -1,4 +1,5 @@ import React from "react"; +import "./StudentsPage.css" function Students() { return
This is the students page
; diff --git a/frontend/src/views/StudentsPage/index.ts b/frontend/src/views/StudentsPage/index.ts new file mode 100644 index 000000000..7bcdecdbb --- /dev/null +++ b/frontend/src/views/StudentsPage/index.ts @@ -0,0 +1 @@ +export { default } from "./StudentsPage"; diff --git a/frontend/src/views/UsersPage/UsersPage.css b/frontend/src/views/UsersPage/UsersPage.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/views/Users.tsx b/frontend/src/views/UsersPage/UsersPage.tsx similarity index 100% rename from frontend/src/views/Users.tsx rename to frontend/src/views/UsersPage/UsersPage.tsx diff --git a/frontend/src/views/UsersPage/index.ts b/frontend/src/views/UsersPage/index.ts new file mode 100644 index 000000000..6f0687e25 --- /dev/null +++ b/frontend/src/views/UsersPage/index.ts @@ -0,0 +1 @@ +export { default } from "./UsersPage"; From 72317fa1a5966cab7b8fd0be386f2c12be74a99c Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 26 Mar 2022 14:07:11 +0100 Subject: [PATCH 083/536] changed some imports --- frontend/src/App.tsx | 6 +++--- frontend/src/components/OSOCLetters/index.ts | 1 + frontend/src/views/LoginPage/LoginPage.tsx | 2 +- frontend/src/views/RegisterPage/RegisterPage.tsx | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/OSOCLetters/index.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e96ebae9e..89ee3ce8d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,10 +4,10 @@ import "./css-files/App.css"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import NavBar from "./components/NavBar"; import LoginPage from "./views/LoginPage"; -import Students from "./views/StudentsPage/StudentsPage"; +import Students from "./views/StudentsPage"; import Users from "./views/UsersPage/UsersPage"; -import ProjectsPage from "./views/ProjectsPage/ProjectsPage"; -import RegisterForm from "./views/RegisterPage/RegisterPage"; +import ProjectsPage from "./views/ProjectsPage"; +import RegisterForm from "./views/RegisterPage"; import ErrorPage from "./views/ErrorPage/ErrorPage"; import PendingPage from "./views/PendingPage"; import Footer from "./components/Footer"; diff --git a/frontend/src/components/OSOCLetters/index.ts b/frontend/src/components/OSOCLetters/index.ts new file mode 100644 index 000000000..4d35943ea --- /dev/null +++ b/frontend/src/components/OSOCLetters/index.ts @@ -0,0 +1 @@ +export { default } from "./OSOCLetters"; diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index 037fe32ed..45ef5b694 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { axiosInstance } from "../../utils/api/api"; -import OSOCLetters from "../../components/OSOCLetters/OSOCLetters"; +import OSOCLetters from "../../components/OSOCLetters"; import "./LoginPage.css"; import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index 719e50edb..03c08a969 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { axiosInstance } from "../../utils/api/api"; -import OSOCLetters from "../../components/OSOCLetters/OSOCLetters"; +import OSOCLetters from "../../components/OSOCLetters"; import "./RegisterPage.css"; import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; From d1b146d5f620b611d4002cc1fedbd0451869fa23 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 26 Mar 2022 14:22:54 +0100 Subject: [PATCH 084/536] cleaned up App.css file --- frontend/src/App.tsx | 2 +- frontend/src/components/navbar/NavBar.tsx | 13 ++++++++++--- ...{NavBarElementss.tsx => NavBarElements.tsx} | 0 frontend/src/components/navbar/index.ts | 2 +- frontend/src/components/navbar/navbar.css | 11 ++++++++++- frontend/src/css-files/App.css | 18 ------------------ frontend/src/views/LoginPage/LoginPage.css | 7 +++++++ .../src/views/StudentsPage/StudentsPage.tsx | 2 +- .../UsersPage/{UsersPage.tsx => Users.tsx} | 0 frontend/src/views/UsersPage/index.ts | 2 +- 10 files changed, 31 insertions(+), 26 deletions(-) rename frontend/src/components/navbar/{NavBarElementss.tsx => NavBarElements.tsx} (100%) rename frontend/src/views/UsersPage/{UsersPage.tsx => Users.tsx} (100%) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 89ee3ce8d..851e6a302 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,7 +5,7 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import NavBar from "./components/NavBar"; import LoginPage from "./views/LoginPage"; import Students from "./views/StudentsPage"; -import Users from "./views/UsersPage/UsersPage"; +import Users from "./views/UsersPage/Users"; import ProjectsPage from "./views/ProjectsPage"; import RegisterForm from "./views/RegisterPage"; import ErrorPage from "./views/ErrorPage/ErrorPage"; diff --git a/frontend/src/components/navbar/NavBar.tsx b/frontend/src/components/navbar/NavBar.tsx index f7a99db69..5bc2cc2a0 100644 --- a/frontend/src/components/navbar/NavBar.tsx +++ b/frontend/src/components/navbar/NavBar.tsx @@ -1,8 +1,8 @@ import React from "react"; -import { Nav, NavLink, Bars, NavMenu } from "./NavBarElementss"; +import { Nav, NavLink, Bars, NavMenu } from "./NavBarElements"; import "./navbar.css"; -function NavBar({ token }: any, {setToken}: any) { +function NavBar({ token }: any, { setToken }: any) { let hidden = "nav-hidden"; if (token) { hidden = "nav-links"; @@ -26,7 +26,14 @@ function NavBar({ token }: any, {setToken}: any) { Students Users Projects - {setToken("")}}>Log out + { + setToken(""); + }} + > + Log out + diff --git a/frontend/src/components/navbar/NavBarElementss.tsx b/frontend/src/components/navbar/NavBarElements.tsx similarity index 100% rename from frontend/src/components/navbar/NavBarElementss.tsx rename to frontend/src/components/navbar/NavBarElements.tsx diff --git a/frontend/src/components/navbar/index.ts b/frontend/src/components/navbar/index.ts index 79cca0c6f..3ab07fb62 100644 --- a/frontend/src/components/navbar/index.ts +++ b/frontend/src/components/navbar/index.ts @@ -1 +1 @@ -export { default } from "./NavBar" \ No newline at end of file +export { default } from "./NavBar"; diff --git a/frontend/src/components/navbar/navbar.css b/frontend/src/components/navbar/navbar.css index f3170af8d..36c48b56c 100644 --- a/frontend/src/components/navbar/navbar.css +++ b/frontend/src/components/navbar/navbar.css @@ -4,4 +4,13 @@ .nav-hidden { visibility: hidden; -} \ No newline at end of file +} + +.logo-plus-name { + color: #fff; + display: flex; + align-items: center; + text-decoration: none; + padding: 0 1rem; + height: 100%; +} diff --git a/frontend/src/css-files/App.css b/frontend/src/css-files/App.css index 9a1d6988f..bee7b3085 100644 --- a/frontend/src/css-files/App.css +++ b/frontend/src/css-files/App.css @@ -12,8 +12,6 @@ height: 100%; } - - * { box-sizing: border-box; margin: 0; @@ -24,19 +22,3 @@ body { background-color: var(--background_color); color: white; } - -.welcome-text { - max-width: 800px; - text-align: center; - justify-content: center; - margin-bottom: 50px; -} - -.logo-plus-name { - color: #fff; - display: flex; - align-items: center; - text-decoration: none; - padding: 0 1rem; - height: 100%; -} diff --git a/frontend/src/views/LoginPage/LoginPage.css b/frontend/src/views/LoginPage/LoginPage.css index c9f495933..0376a8286 100644 --- a/frontend/src/views/LoginPage/LoginPage.css +++ b/frontend/src/views/LoginPage/LoginPage.css @@ -6,6 +6,13 @@ flex-direction: column; } +.welcome-text { + max-width: 800px; + text-align: center; + justify-content: center; + margin-bottom: 50px; +} + .socials-container { display: flex; justify-content: center; diff --git a/frontend/src/views/StudentsPage/StudentsPage.tsx b/frontend/src/views/StudentsPage/StudentsPage.tsx index 162122f17..5a158d93d 100644 --- a/frontend/src/views/StudentsPage/StudentsPage.tsx +++ b/frontend/src/views/StudentsPage/StudentsPage.tsx @@ -1,5 +1,5 @@ import React from "react"; -import "./StudentsPage.css" +import "./StudentsPage.css"; function Students() { return
This is the students page
; diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/Users.tsx similarity index 100% rename from frontend/src/views/UsersPage/UsersPage.tsx rename to frontend/src/views/UsersPage/Users.tsx diff --git a/frontend/src/views/UsersPage/index.ts b/frontend/src/views/UsersPage/index.ts index 6f0687e25..d25de51bd 100644 --- a/frontend/src/views/UsersPage/index.ts +++ b/frontend/src/views/UsersPage/index.ts @@ -1 +1 @@ -export { default } from "./UsersPage"; +export { default } from "./Users"; From c70b6cdfbbed08cb7943ef38ea6c2fe76b20dec1 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 26 Mar 2022 14:40:58 +0100 Subject: [PATCH 085/536] import update --- frontend/src/App.tsx | 4 ++-- frontend/src/views/UsersPage/{Users.tsx => UsersPage.tsx} | 0 frontend/src/views/UsersPage/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename frontend/src/views/UsersPage/{Users.tsx => UsersPage.tsx} (100%) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 851e6a302..b2b36129e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,10 +5,10 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import NavBar from "./components/NavBar"; import LoginPage from "./views/LoginPage"; import Students from "./views/StudentsPage"; -import Users from "./views/UsersPage/Users"; +import Users from "./views/UsersPage"; import ProjectsPage from "./views/ProjectsPage"; import RegisterForm from "./views/RegisterPage"; -import ErrorPage from "./views/ErrorPage/ErrorPage"; +import ErrorPage from "./views/ErrorPage"; import PendingPage from "./views/PendingPage"; import Footer from "./components/Footer"; import { Container, ContentWrapper } from "./app.styles"; diff --git a/frontend/src/views/UsersPage/Users.tsx b/frontend/src/views/UsersPage/UsersPage.tsx similarity index 100% rename from frontend/src/views/UsersPage/Users.tsx rename to frontend/src/views/UsersPage/UsersPage.tsx diff --git a/frontend/src/views/UsersPage/index.ts b/frontend/src/views/UsersPage/index.ts index d25de51bd..6f0687e25 100644 --- a/frontend/src/views/UsersPage/index.ts +++ b/frontend/src/views/UsersPage/index.ts @@ -1 +1 @@ -export { default } from "./Users"; +export { default } from "./UsersPage"; From 293bbb4ddc45f81b6f7fead5abfbd230213387af Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Sat, 26 Mar 2022 14:47:40 +0100 Subject: [PATCH 086/536] added test --- .../test_projects/test_projects.py | 374 +++++++++--------- .../test_projects/test_students/__init__.py | 0 .../test_students/test_students.py | 218 ++++++++++ 3 files changed, 407 insertions(+), 185 deletions(-) create mode 100644 backend/tests/test_routers/test_editions/test_projects/test_students/__init__.py create mode 100644 backend/tests/test_routers/test_editions/test_projects/test_students/test_students.py 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 2d8cea311..3bbfaf021 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,255 +1,259 @@ - +import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session from starlette import status -from src.app.logic.projects_students import logic_add_student_project -from src.database.models import Edition, Project, Student, Skill, User +from src.database.models import Edition, Project, User, Skill, ProjectRole, Student, Partner -def test_get_projects(database_session: Session, test_client: TestClient): - database_session.add(Edition(year=2022)) - project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) - database_session.add(project) +@pytest.fixture +def database_with_data(database_session: Session) -> Session: + """fixture for adding data to the database""" + edition: Edition = Edition(year=2022) + 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) + user: User = User(name="coach1", email="user@user.be") + 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() - response = test_client.get("/editions/1/projects") - json = response.json() + return database_session - assert len(json['projects']) == 1 - assert json['projects'][0]['name'] == "project" +@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_project(database_session: Session, test_client: TestClient): - database_session.add(Edition(year=2022)) - project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) - database_session.add(project) - database_session.commit() - response = test_client.get("/editions/1/projects/1") +def test_get_projects(database_with_data: Session, test_client: TestClient): + """Tests get all projects""" + response = test_client.get("/editions/1/projects") json = response.json() - assert json['name'] == 'project' + assert len(json['projects']) == 3 + assert json['projects'][0]['name'] == "project1" + assert json['projects'][1]['name'] == "project2" + assert json['projects'][2]['name'] == "project3" -def test_delete_project(database_session: Session, test_client: TestClient): - database_session.add(Edition(year=2022)) - project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) - database_session.add(project) - database_session.commit() +def test_get_project(database_with_data: Session, test_client: TestClient): + """Tests get a specific project""" + response = test_client.get("/editions/1/projects/1") + assert response.status_code == status.HTTP_200_OK + json = response.json() + print(json) + assert json['name'] == 'project1' - response = test_client.delete("/editions/1/projects/1") +def test_delete_project(database_with_data: Session, test_client: TestClient): + """Tests delete a project""" + response = test_client.get("/editions/1/projects/1") + assert response.status_code == status.HTTP_200_OK + response = test_client.delete("/editions/1/projects/1") assert response.status_code == status.HTTP_204_NO_CONTENT + response = test_client.get("/editions/1/projects/1") + assert response.status_code == status.HTTP_404_NOT_FOUND -def test_delete_no_projects(database_session: Session, test_client: TestClient): - database_session.add(Edition(year=2022)) - database_session.commit() - - response = test_client.delete("/editions/1/projects/1") - +def test_delete_ghost_project(database_with_data: Session, test_client: TestClient): + """Tests delete a project that don't exist""" + response = test_client.get("/editions/1/projects/400") + assert response.status_code == status.HTTP_404_NOT_FOUND + response = test_client.delete("/editions/1/projects/400") assert response.status_code == status.HTTP_404_NOT_FOUND -def test_create_project(database_session: Session, test_client: TestClient): - database_session.add(Edition(year=2022)) - database_session.commit() +def test_create_project(database_with_data: Session, test_client: TestClient): + """test create a project""" + response = test_client.get('/editions/1/projects') + json = response.json() + assert len(json['projects']) == 3 + assert len(database_with_data.query(Partner).all()) == 0 response = \ test_client.post("/editions/1/projects/", json={"name": "test", "number_of_students": 5, - "skills": [], "partners": [], "coaches": []}) + "skills": [1, 1, 1, 1, 1], "partners": ["ugent"], "coaches": [1]}) assert response.status_code == status.HTTP_201_CREATED - assert response.json()['projectId'] == 1 assert response.json()['name'] == 'test' + assert response.json()["partners"][0]["name"] == "ugent" - response2 = test_client.get('/editions/1/projects') - json = response2.json() - - assert len(json['projects']) == 1 - assert json['projects'][0]['name'] == "test" - - -def test_create_wrong_project(database_session: Session, test_client: TestClient): - database_session.add(Edition(year=2022)) - database_session.commit() + assert len(database_with_data.query(Partner).all()) == 1 + response = test_client.get('/editions/1/projects') + json = response.json() - response = \ - test_client.post("/editions/1/projects/", - # project has no name - json={ - "number_of_students": 5, - "skills": [], "partners": [], "coaches": []}) + assert len(json['projects']) == 4 + assert json['projects'][3]['name'] == "test" - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - response2 = test_client.get('/editions/1/projects') - json = response2.json() - - assert len(json['projects']) == 0 +def test_create_project_same_partner(database_with_data: Session, test_client: TestClient): + """Tests that creating a project, don't create a partner if the partner allready exist""" + assert len(database_with_data.query(Partner).all()) == 0 + test_client.post("/editions/1/projects/", + json={"name": "test1", + "number_of_students": 2, + "skills": [1, 2], "partners": ["ugent"], "coaches": [1]}) + test_client.post("/editions/1/projects/", + json={"name": "test2", + "number_of_students": 2, + "skills": [1, 2], "partners": ["ugent"], "coaches": [1]}) + assert len(database_with_data.query(Partner).all()) == 1 -def test_patch_project(database_session: Session, test_client: TestClient): - database_session.add(Edition(year=2022)) - project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) - database_session.add(project) - database_session.commit() +def test_create_project_non_existing_skills(database_with_data: Session, test_client: TestClient): + """Tests creating a project with non existing skills""" + response = test_client.get('/editions/1/projects') + json = response.json() + assert len(json['projects']) == 3 + + assert len(database_with_data.query(Skill).where( + Skill.skill_id == 100).all()) == 0 + response = test_client.post("/editions/1/projects/", + json={"name": "test1", + "number_of_students": 1, + "skills": [100], "partners": ["ugent"], "coaches": [1]}) + assert response.status_code == status.HTTP_404_NOT_FOUND - response = \ - test_client.patch("/editions/1/projects/1", - json={"name": "patched", - "number_of_students": 5, - "skills": [], "partners": [], "coaches": []}) - assert response.status_code == status.HTTP_204_NO_CONTENT + response = test_client.get('/editions/1/projects') + json = response.json() + assert len(json['projects']) == 3 - response2 = test_client.get('/editions/1/projects') - json = response2.json() - assert len(json['projects']) == 1 - assert json['projects'][0]['name'] == 'patched' +def test_create_project_non_existing_coach(database_with_data: Session, test_client: TestClient): + """test create a project with a coach that don't exist""" + response = test_client.get('/editions/1/projects') + json = response.json() + assert len(json['projects']) == 3 + + assert len(database_with_data.query(Student).where( + Student.edition_id == 10).all()) == 0 + response = test_client.post("/editions/1/projects/", + json={"name": "test2", + "number_of_students": 1, + "skills": [100], "partners": ["ugent"], "coaches": [10]}) + assert response.status_code == status.HTTP_404_NOT_FOUND + response = test_client.get('/editions/1/projects') + json = response.json() + assert len(json['projects']) == 3 -def test_patch_wrong_project(database_session: Session, test_client: TestClient): - database_session.add(Edition(year=2022)) - project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) - database_session.add(project) - database_session.commit() +def test_create_project_no_name(database_with_data: Session, test_client: TestClient): + """Tests when creating a project that has no name""" + response = test_client.get('/editions/1/projects') + json = response.json() + assert len(json['projects']) == 3 response = \ - test_client.patch("/editions/1/projects/1", - json={"name": "patched", - "skills": [], "partners": [], "coaches": []}) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - - response2 = test_client.get('/editions/1/projects') - json = response2.json() - - assert len(json['projects']) == 1 - assert json['projects'][0]['name'] == 'project' - - -def test_add_student_project(database_session: Session, test_client: TestClient): - database_session.add(Edition(year=2022)) - project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) - database_session.add(project) - database_session.commit() - - resp = test_client.post("/editions/1/projects/1/students/1", json={"skill_id": 1, "drafter_id": 1}) - - assert resp.status_code == status.HTTP_201_CREATED - - response2 = test_client.get('/editions/1/projects') - json = response2.json() - - assert len(json['projects'][0]['projectRoles']) == 1 - assert json['projects'][0]['projectRoles'][0]['skillId'] == 1 - - -def test_add_wrong_student_project(database_session: Session, test_client: TestClient): - database_session.add(Edition(year=2022)) - project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) - database_session.add(project) - database_session.commit() - - resp = test_client.post("/editions/1/projects/1/students/1", json={"drafter_id": 1}) + test_client.post("/editions/1/projects/", + # project has no name + json={ + "number_of_students": 5, + "skills": [], "partners": [], "coaches": []}) - assert resp.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - response2 = test_client.get('/editions/1/projects') - json = response2.json() + response = test_client.get('/editions/1/projects') + json = response.json() + assert len(json['projects']) == 3 - assert len(json['projects'][0]['projectRoles']) == 0 +def test_patch_project(database_with_data: Session, test_client: TestClient): + """test patch a project""" -def test_change_student_project(database_session: Session, test_client: TestClient): - database_session.add(Edition(year=2022)) - project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) - database_session.add(project) - database_session.commit() + response = test_client.get('/editions/1/projects') + json = response.json() - logic_add_student_project(database_session, project, 1, 1, 1) - resp1 = test_client.patch("/editions/1/projects/1/students/1", json={"skill_id": 2, "drafter_id": 2}) + assert len(json['projects']) == 3 - assert resp1.status_code == status.HTTP_204_NO_CONTENT + response = test_client.patch("/editions/1/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 - response2 = test_client.get('/editions/1/projects') - json = response2.json() + response = test_client.get('/editions/1/projects') + json = response.json() - assert len(json['projects'][0]['projectRoles']) == 1 - assert json['projects'][0]['projectRoles'][0]['skillId'] == 2 + assert len(json['projects']) == 3 + assert json['projects'][0]['name'] == 'patched' -def test_change_wrong_student_project(database_session: Session, test_client: TestClient): - database_session.add(Edition(year=2022)) - project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) - database_session.add(project) - database_session.commit() +def test_patch_project_non_existing_skills(database_with_data: Session, test_client: TestClient): + """Tests patch a project with non existing skills""" + assert len(database_with_data.query(Skill).where( + Skill.skill_id == 100).all()) == 0 + response = test_client.patch("/editions/1/projects/1", + json={"name": "test1", + "number_of_students": 1, + "skills": [100], "partners": ["ugent"], "coaches": [1]}) + assert response.status_code == status.HTTP_404_NOT_FOUND - logic_add_student_project(database_session, project, 1, 1, 1) - resp1 = test_client.patch("/editions/1/projects/1/students/1", json={"skill_id": 2}) + response = test_client.get("/editions/1/projects/1") + json = response.json() + print(json) + assert 100 not in json["skills"] - assert resp1.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - response2 = test_client.get('/editions/1/projects') - json = response2.json() +def test_patch_project_non_existing_coach(database_with_data: Session, test_client: TestClient): + """test patch a project with a coach that don't exist""" - assert len(json['projects'][0]['projectRoles']) == 1 - assert json['projects'][0]['projectRoles'][0]['skillId'] == 1 + assert len(database_with_data.query(Student).where( + Student.edition_id == 10).all()) == 0 + response = test_client.patch("/editions/1/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 = test_client.get("/editions/1/projects/1") + json = response.json() + print(json) + assert 10 not in json["coaches"] -def test_delete_student_project(database_session: Session, test_client: TestClient): +def test_patch_wrong_project(database_session: Session, test_client: TestClient): + """tests patch with wrong project info""" database_session.add(Edition(year=2022)) - project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) + project = Project(name="project", edition_id=1, + project_id=1, number_of_students=2) database_session.add(project) database_session.commit() - logic_add_student_project(database_session, project, 1, 1, 1) - resp = test_client.delete("/editions/1/projects/1/students/1") - - assert resp.status_code == status.HTTP_204_NO_CONTENT + response = \ + test_client.patch("/editions/1/projects/1", + json={"name": "patched", + "skills": [], "partners": [], "coaches": []}) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY response2 = test_client.get('/editions/1/projects') json = response2.json() - assert len(json['projects'][0]['projectRoles']) == 0 - - -def test_delete_student_project_empty(database_session: Session, test_client: TestClient): - database_session.add(Edition(year=2022)) - project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) - database_session.add(project) - database_session.commit() - - resp = test_client.delete("/editions/1/projects/1/students/1") - - assert resp.status_code == status.HTTP_404_NOT_FOUND - - -def test_get_conflicts(database_session: Session, test_client: TestClient): - database_session.add(Edition(year=2022)) - project = Project(name="project", edition_id=1, project_id=1, number_of_students=1) - project2 = Project(name="project2", edition_id=1, project_id=3, number_of_students=1) - student = Student(student_id=1, first_name="test", last_name="person", preferred_name="test", - email_address="a@b.com", - alumni=False, edition_id=1) - skill = Skill(skill_id=1, name="test_skill") - skill2 = Skill(skill_id=2, name="test_skill2") - user = User(user_id=1, name="testuser", email="b@c.com") - database_session.add(project) - database_session.add(project2) - database_session.add(student) - database_session.add(skill) - database_session.add(skill2) - database_session.add(user) - database_session.commit() - - logic_add_student_project(database_session, project, 1, 1, 1) - logic_add_student_project(database_session, project2, 1, 2, 1) - response = test_client.get("/editions/1/projects/conflicts") - json = response.json() - assert len(json['conflictStudents']) == 1 - assert json['conflictStudents'][0]['student']['studentId'] == 1 - assert len(json['conflictStudents'][0]['projects']) == 2 + assert len(json['projects']) == 1 + assert json['projects'][0]['name'] == 'project' diff --git a/backend/tests/test_routers/test_editions/test_projects/test_students/__init__.py b/backend/tests/test_routers/test_editions/test_projects/test_students/__init__.py new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..875557d49 --- /dev/null +++ b/backend/tests/test_routers/test_editions/test_projects/test_students/test_students.py @@ -0,0 +1,218 @@ +import pytest +from fastapi.testclient import TestClient +from sqlalchemy.orm import Session +from starlette import status + +from src.database.models import Edition, Project, User, Skill, ProjectRole, Student, Partner + + +@pytest.fixture +def database_with_data(database_session: Session) -> Session: + """fixture for adding data to the database""" + edition: Edition = Edition(year=2022) + 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) + user: User = User(name="coach1", email="user@user.be") + 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]) + 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]) + database_session.add(student01) + database_session.add(student02) + database_session.add(student03) + 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_add_student_project(database_with_data: Session, test_client: TestClient): + """tests add a student to a project""" + resp = test_client.post( + "/editions/1/projects/1/students/3", json={"skill_id": 1, "drafter_id": 1}) + + assert resp.status_code == status.HTTP_201_CREATED + + response2 = test_client.get('/editions/1/projects') + json = response2.json() + + assert len(json['projects'][0]['projectRoles']) == 3 + assert json['projects'][0]['projectRoles'][2]['skillId'] == 1 + + +def test_add_ghost_student_project(database_with_data: Session, test_client: TestClient): + """tests add a non existing student to a project""" + student10: list[Student] = database_with_data.query( + Student).where(Student.student_id == 10).all() + assert len(student10) == 0 + response = test_client.get('/editions/1/projects/1') + json = response.json() + assert len(json['projectRoles']) == 2 + + resp = test_client.post( + "/editions/1/projects/1/students/3", json={"skill_id": 1, "drafter_id": 1}) + assert resp.status_code == status.HTTP_404_NOT_FOUND + + response = test_client.get('/editions/1/projects/1') + json = response.json() + assert len(json['projectRoles']) == 2 + + +def test_add_student_project_non_existing_skill(database_with_data: Session, test_client: TestClient): + """tests add a non existing student to a project""" + skill10: list[Skill] = database_with_data.query( + Skill).where(Skill.skill_id == 10).all() + assert len(skill10) == 0 + response = test_client.get('/editions/1/projects/1') + json = response.json() + assert len(json['projectRoles']) == 2 + + resp = test_client.post( + "/editions/1/projects/1/students/3", json={"skill_id": 10, "drafter_id": 1}) + assert resp.status_code == status.HTTP_404_NOT_FOUND + + response = test_client.get('/editions/1/projects/1') + json = response.json() + assert len(json['projectRoles']) == 2 + + +def test_add_student_project_ghost_drafter(database_with_data: Session, test_client: TestClient): + """test add a student to a project with a drafter that don't exist""" + user10: list[User] = database_with_data.query( + User).where(User.user_id == 10).all() + assert len(user10) == 0 + + response = test_client.get('/editions/1/projects/1') + json = response.json() + assert len(json['projectRoles']) == 2 + + resp = test_client.post( + "/editions/1/projects/1/students/3", json={"skill_id": 1, "drafter_id": 10}) + assert resp.status_code == status.HTTP_404_NOT_FOUND + + response = test_client.get('/editions/1/projects/1') + json = response.json() + assert len(json['projectRoles']) == 2 + + +def test_add_student_to_ghost_project(database_with_data: Session, test_client: TestClient): + """test add a student to a project that don't exist""" + project10: list[Project] = database_with_data.query( + Project).where(Project.project_id == 10).all() + assert len(project10) == 0 + + resp = test_client.post( + "/editions/1/projects/10/students/1", json={"skill_id": 1, "drafter_id": 1}) + assert resp.status_code == status.HTTP_404_NOT_FOUND + + +def test_add_incomplete_data_student_project(database_session: Session, test_client: TestClient): + """test add a student with incomplete data""" + database_session.add(Edition(year=2022)) + project = Project(name="project", edition_id=1, + project_id=1, number_of_students=2) + database_session.add(project) + database_session.commit() + + resp = test_client.post( + "/editions/1/projects/1/students/1", json={"drafter_id": 1}) + + assert resp.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + response2 = test_client.get('/editions/1/projects') + json = response2.json() + + assert len(json['projects'][0]['projectRoles']) == 0 + + +def test_change_student_project(database_with_data: Session, test_client: TestClient): + """test change a student project""" + resp1 = test_client.patch( + "/editions/1/projects/1/students/1", json={"skill_id": 2, "drafter_id": 2}) + + assert resp1.status_code == status.HTTP_204_NO_CONTENT + + response2 = test_client.get('/editions/1/projects') + json = response2.json() + + assert len(json['projects'][0]['projectRoles']) == 2 + assert json['projects'][0]['projectRoles'][0]['skillId'] == 2 + + +def test_change_incomplete_data_student_project(database_with_data: Session, test_client: TestClient): + """test change student project with incomplete data""" + resp1 = test_client.patch( + "/editions/1/projects/1/students/1", json={"skill_id": 2}) + + assert resp1.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + response2 = test_client.get('/editions/1/projects') + json = response2.json() + + assert len(json['projects'][0]['projectRoles']) == 2 + assert json['projects'][0]['projectRoles'][0]['skillId'] == 1 + + +def test_delete_student_project(database_with_data: Session, test_client: TestClient): + """test delete a student from a project""" + resp = test_client.delete("/editions/1/projects/1/students/1") + + assert resp.status_code == status.HTTP_204_NO_CONTENT + + response2 = test_client.get('/editions/1/projects') + json = response2.json() + + assert len(json['projects'][0]['projectRoles']) == 1 + + +def test_delete_student_project_empty(database_session: Session, test_client: TestClient): + """delete a student from a project that isn't asigned""" + database_session.add(Edition(year=2022)) + project = Project(name="project", edition_id=1, + project_id=1, number_of_students=2) + database_session.add(project) + database_session.commit() + + resp = test_client.delete("/editions/1/projects/1/students/1") + + assert resp.status_code == status.HTTP_404_NOT_FOUND + + +def test_get_conflicts(database_with_data: Session, test_client: TestClient): + """test get the conflicts""" + response = test_client.get("/editions/1/projects/conflicts") + json = response.json() + assert len(json['conflictStudents']) == 1 + assert json['conflictStudents'][0]['student']['studentId'] == 1 + assert len(json['conflictStudents'][0]['projects']) == 2 From 2df8dfce293748d7729aafa6cf1de3edfe6c4d2b Mon Sep 17 00:00:00 2001 From: beguille Date: Sat, 26 Mar 2022 15:49:17 +0100 Subject: [PATCH 087/536] fixes #173, validated some fields of projects_students --- .../src/database/crud/projects_students.py | 14 +++- .../test_students/test_students.py | 70 ++++++++++++++++++- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/backend/src/database/crud/projects_students.py b/backend/src/database/crud/projects_students.py index c6d2bd65c..5ba5a79e5 100644 --- a/backend/src/database/crud/projects_students.py +++ b/backend/src/database/crud/projects_students.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session -from src.database.models import Project, ProjectRole +from src.database.models import Project, ProjectRole, Skill, User, Student def db_remove_student_project(db: Session, project: Project, student_id: int): @@ -13,6 +13,12 @@ def db_remove_student_project(db: Session, project: Project, student_id: int): def db_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() + proj_role = ProjectRole(student_id=student_id, project_id=project.project_id, skill_id=skill_id, drafter_id=drafter_id) db.add(proj_role) @@ -21,6 +27,12 @@ def db_add_student_project(db: Session, project: Project, student_id: int, skill def db_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() + proj_role = db.query(ProjectRole).where( ProjectRole.student_id == student_id).where(ProjectRole.project == project).one() proj_role.drafter_id = drafter_id 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 875557d49..1dcd3991e 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 @@ -81,7 +81,7 @@ def test_add_ghost_student_project(database_with_data: Session, test_client: Tes assert len(json['projectRoles']) == 2 resp = test_client.post( - "/editions/1/projects/1/students/3", json={"skill_id": 1, "drafter_id": 1}) + "/editions/1/projects/1/students/10", json={"skill_id": 1, "drafter_id": 1}) assert resp.status_code == status.HTTP_404_NOT_FOUND response = test_client.get('/editions/1/projects/1') @@ -159,7 +159,7 @@ def test_add_incomplete_data_student_project(database_session: Session, test_cli def test_change_student_project(database_with_data: Session, test_client: TestClient): """test change a student project""" resp1 = test_client.patch( - "/editions/1/projects/1/students/1", json={"skill_id": 2, "drafter_id": 2}) + "/editions/1/projects/1/students/1", json={"skill_id": 2, "drafter_id": 1}) assert resp1.status_code == status.HTTP_204_NO_CONTENT @@ -184,6 +184,72 @@ def test_change_incomplete_data_student_project(database_with_data: Session, tes assert json['projects'][0]['projectRoles'][0]['skillId'] == 1 +def test_change_ghost_student_project(database_with_data: Session, test_client: TestClient): + """tests change a non existing student of a project""" + student10: list[Student] = database_with_data.query( + Student).where(Student.student_id == 10).all() + assert len(student10) == 0 + response = test_client.get('/editions/1/projects/1') + json = response.json() + assert len(json['projectRoles']) == 2 + + resp = test_client.patch( + "/editions/1/projects/1/students/10", json={"skill_id": 1, "drafter_id": 1}) + assert resp.status_code == status.HTTP_404_NOT_FOUND + + response = test_client.get('/editions/1/projects/1') + json = response.json() + assert len(json['projectRoles']) == 2 + + +def test_change_student_project_non_existing_skill(database_with_data: Session, test_client: TestClient): + """test change a skill of a projectRole to a non-existing one""" + skill10: list[Skill] = database_with_data.query( + Skill).where(Skill.skill_id == 10).all() + assert len(skill10) == 0 + response = test_client.get('/editions/1/projects/1') + json = response.json() + assert len(json['projectRoles']) == 2 + + resp = test_client.patch( + "/editions/1/projects/1/students/3", json={"skill_id": 10, "drafter_id": 1}) + assert resp.status_code == status.HTTP_404_NOT_FOUND + + response = test_client.get('/editions/1/projects/1') + json = response.json() + assert len(json['projectRoles']) == 2 + + +def test_change_student_project_ghost_drafter(database_with_data: Session, test_client: TestClient): + """test change a drafter of a projectRole to a non-existing one""" + user10: list[User] = database_with_data.query( + User).where(User.user_id == 10).all() + assert len(user10) == 0 + + response = test_client.get('/editions/1/projects/1') + json = response.json() + assert len(json['projectRoles']) == 2 + + resp = test_client.patch( + "/editions/1/projects/1/students/3", json={"skill_id": 1, "drafter_id": 10}) + assert resp.status_code == status.HTTP_404_NOT_FOUND + + response = test_client.get('/editions/1/projects/1') + json = response.json() + assert len(json['projectRoles']) == 2 + + +def test_change_student_to_ghost_project(database_with_data: Session, test_client: TestClient): + """test change a student of a project that don't exist""" + project10: list[Project] = database_with_data.query( + Project).where(Project.project_id == 10).all() + assert len(project10) == 0 + + resp = test_client.patch( + "/editions/1/projects/10/students/1", json={"skill_id": 1, "drafter_id": 1}) + assert resp.status_code == status.HTTP_404_NOT_FOUND + + def test_delete_student_project(database_with_data: Session, test_client: TestClient): """test delete a student from a project""" resp = test_client.delete("/editions/1/projects/1/students/1") From 6c8814247eabac41de7ac3c17d0242f77f2c5a70 Mon Sep 17 00:00:00 2001 From: beguille Date: Sat, 26 Mar 2022 16:15:37 +0100 Subject: [PATCH 088/536] closes #169, a JWT token is now valid for one week --- backend/.env.example | 2 ++ backend/settings.py | 2 +- backend/src/app/logic/security.py | 7 ++----- backend/src/app/routers/login/login.py | 7 ++++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index e3c24e158..ec2a58e0b 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -7,6 +7,8 @@ DB_PORT=3306 # JWT key (needs to be changed for production) # Can be generated using "openssl rand -hex 32" SECRET_KEY=4d16a9cc83d74144322e893c879b5f639088c15dc1606b11226abbd7e97f5ee5 +# The JWT token should be valid for 24*7(=168) hours +ACCESS_TOKEN_EXPIRE_HOURS = 168 # Frontend FRONTEND_URL="http://localhost:3000" \ No newline at end of file diff --git a/backend/settings.py b/backend/settings.py index 4cc403067..fe64b105f 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -29,7 +29,7 @@ """JWT token key""" SECRET_KEY: str = env.str("SECRET_KEY", "4d16a9cc83d74144322e893c879b5f639088c15dc1606b11226abbd7e97f5ee5") - +ACCESS_TOKEN_EXPIRE_HOURS: int = env.str("ACCESS_TOKEN_EXPIRE_HOURS", 168) """Frontend""" FRONTEND_URL: str = env.str("FRONTEND_URL", "http://localhost:3000") diff --git a/backend/src/app/logic/security.py b/backend/src/app/logic/security.py index 4c1e52484..32aed5bfb 100644 --- a/backend/src/app/logic/security.py +++ b/backend/src/app/logic/security.py @@ -1,15 +1,12 @@ from datetime import timedelta, datetime -from fastapi import Depends from jose import jwt from passlib.context import CryptContext from sqlalchemy.orm import Session +import settings from src.app.exceptions.authentication import InvalidCredentialsException from src.database import models -import settings - -from src.database.database import get_session # Configuration ALGORITHM = "HS256" @@ -25,7 +22,7 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None) -> s if expires_delta is not None: expire = datetime.utcnow() + expires_delta else: - expire = datetime.utcnow() + timedelta(hours=24) + expire = datetime.utcnow() + timedelta(hours=settings.ACCESS_TOKEN_EXPIRE_HOURS) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM) diff --git a/backend/src/app/routers/login/login.py b/backend/src/app/routers/login/login.py index d2f37dcce..c46099c17 100644 --- a/backend/src/app/routers/login/login.py +++ b/backend/src/app/routers/login/login.py @@ -6,8 +6,9 @@ from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.orm import Session +import settings from src.app.exceptions.authentication import InvalidCredentialsException -from src.app.logic.security import authenticate_user, ACCESS_TOKEN_EXPIRE_HOURS, create_access_token +from src.app.logic.security import authenticate_user, create_access_token from src.app.routers.tags import Tags from src.app.schemas.login import Token from src.database.database import get_session @@ -25,8 +26,8 @@ async def login_for_access_token(db: Session = Depends(get_session), # Don't use our own error handler here because this should # be a 401 instead of a 404 raise InvalidCredentialsException() from not_found - - access_token_expires = timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS) + + access_token_expires = timedelta(hours=settings.ACCESS_TOKEN_EXPIRE_HOURS) access_token = create_access_token( data={"sub": str(user.user_id)}, expires_delta=access_token_expires ) From ab530f25ce3fb6cff8a4ea5553f86a14e3be262f Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 26 Mar 2022 16:24:08 +0100 Subject: [PATCH 089/536] moved login axios code to utils/api --- frontend/src/App.tsx | 8 +++--- frontend/src/utils/api/login.ts | 20 ++++++++++++++ frontend/src/views/LoginPage/LoginPage.tsx | 31 +++++++--------------- frontend/src/views/UsersPage/UsersPage.tsx | 4 +-- 4 files changed, 36 insertions(+), 27 deletions(-) create mode 100644 frontend/src/utils/api/login.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b2b36129e..64d923b33 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,8 +4,8 @@ import "./css-files/App.css"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import NavBar from "./components/NavBar"; import LoginPage from "./views/LoginPage"; -import Students from "./views/StudentsPage"; -import Users from "./views/UsersPage"; +import StudentsPage from "./views/StudentsPage"; +import UsersPage from "./views/UsersPage"; import ProjectsPage from "./views/ProjectsPage"; import RegisterForm from "./views/RegisterPage"; import ErrorPage from "./views/ErrorPage"; @@ -25,8 +25,8 @@ function App() { } /> } /> - } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/frontend/src/utils/api/login.ts b/frontend/src/utils/api/login.ts new file mode 100644 index 000000000..3717e96b5 --- /dev/null +++ b/frontend/src/utils/api/login.ts @@ -0,0 +1,20 @@ +import axios from "axios"; +import { axiosInstance } from "./api"; + +export async function logIn({ setToken }: any, email: any, password: any) { + const payload = new FormData(); + payload.append("username", email); + payload.append("password", password); + try { + await axiosInstance.post("/login/token", payload).then((response: any) => { + setToken(response.data.accessToken); + }); + return true; + } catch (error) { + if (axios.isAxiosError(error)) { + return false; + } else { + throw error; + } + } +} diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index 45ef5b694..ffdcb1187 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -1,32 +1,23 @@ import { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { axiosInstance } from "../../utils/api/api"; import OSOCLetters from "../../components/OSOCLetters"; import "./LoginPage.css"; +import { logIn } from "../../utils/api/login"; +import { useNavigate } from "react-router-dom"; import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; function LoginPage({ setToken }: any) { - function logIn() { - const payload = new FormData(); - payload.append("username", email); - payload.append("password", password); - - axiosInstance - .post("/login/token", payload) - .then((response: any) => { - setToken(response.data.accessToken); - }) - .then(() => navigate("/students")) - .catch(function (error: any) { - console.log(error); - }); - } const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); - const navigate = useNavigate(); + function callLogIn() { + logIn({ setToken }, email, password).then(response => { + if (response) navigate("/students"); + else alert("Login failed"); + }); + } + return (
@@ -40,7 +31,6 @@ function LoginPage({ setToken }: any) {
-
@@ -50,7 +40,6 @@ function LoginPage({ setToken }: any) {
-
-
diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index 77d2905c0..b96c7fc57 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -1,7 +1,7 @@ import React from "react"; -function Users() { +function UsersPage() { return
This is the users page
; } -export default Users; +export default UsersPage; From ed31df2e8c6f3b5a10fd8ccf9505d5ff97e9947d Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 26 Mar 2022 16:35:12 +0100 Subject: [PATCH 090/536] merged with master --- frontend/src/App.tsx | 6 +- .../src/views/RegisterForm/RegisterForm.tsx | 127 --------------- .../src/views/RegisterPage/RegisterPage.tsx | 151 ++++++++++-------- 3 files changed, 86 insertions(+), 198 deletions(-) delete mode 100644 frontend/src/views/RegisterForm/RegisterForm.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 64d923b33..278ed30e4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,12 +2,12 @@ import React, { useState } from "react"; import "bootstrap/dist/css/bootstrap.min.css"; import "./css-files/App.css"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; -import NavBar from "./components/NavBar"; +import NavBar from "./components/navbar"; import LoginPage from "./views/LoginPage"; import StudentsPage from "./views/StudentsPage"; import UsersPage from "./views/UsersPage"; import ProjectsPage from "./views/ProjectsPage"; -import RegisterForm from "./views/RegisterPage"; +import RegisterPage from "./views/RegisterPage"; import ErrorPage from "./views/ErrorPage"; import PendingPage from "./views/PendingPage"; import Footer from "./components/Footer"; @@ -24,7 +24,7 @@ function App() { } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/views/RegisterForm/RegisterForm.tsx b/frontend/src/views/RegisterForm/RegisterForm.tsx deleted file mode 100644 index 9179c33ac..000000000 --- a/frontend/src/views/RegisterForm/RegisterForm.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; -import { axiosInstance } from "../../utils/api/api"; - -import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; - -interface RegisterFields { - email: string; - name: string; - uuid: string; - pw: string; -} - -function RegisterForm() { - function register(uuid: string) { - // Check if passwords are the same - if (password !== confirmPassword) { - alert("Passwords do not match"); - return; - } - // Basic email checker - if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { - alert("This is not a valid email"); - return; - } - - // TODO this has to change to get the edition the invite belongs to - const edition = "1"; - const payload: RegisterFields = { email: email, name: name, uuid: uuid, pw: password }; - - axiosInstance - .post("/editions/" + edition + "/register/email", payload) - .then((response: any) => console.log(response)) - .then(() => navigate("/pending")) - .catch(function (error: any) { - console.log(error); - }); - } - - const [email, setEmail] = useState(""); - const [name, setName] = useState(""); - const [password, setPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - - const navigate = useNavigate(); - - const params = useParams(); - const uuid = params.uuid; - - const [validUuid, setUuid] = useState(false); - - axiosInstance.get("/editions/" + 1 + "/invites/" + uuid).then(response => { - if (response.data.uuid === uuid) { - setUuid(true); - } - }); - - if (validUuid && uuid) { - return ( -
-
-

Create an account

- -
- Sign up with your social media account or email address. Your unique link is - not useable again ({uuid}) -
-
-
- -
- -
-
- -

or

- -
-
- setEmail(e.target.value)} - /> -
-
- setName(e.target.value)} - /> -
-
- setPassword(e.target.value)} - /> -
-
- setConfirmPassword(e.target.value)} - /> -
-
-
- -
-
-
- ); - } else return
Not a valid register url
; -} - -export default RegisterForm; diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index 03c08a969..dc0011ed9 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -1,31 +1,32 @@ import { useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { axiosInstance } from "../../utils/api/api"; -import OSOCLetters from "../../components/OSOCLetters"; -import "./RegisterPage.css"; import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; -function RegisterForm() { - function register() { +interface RegisterFields { + email: string; + name: string; + uuid: string; + pw: string; +} + +function RegisterPage() { + function register(uuid: string) { // Check if passwords are the same if (password !== confirmPassword) { alert("Passwords do not match"); return; } // Basic email checker - if (!/^\w+([\\.-]?\w+-)*@\w+([\\.-]?\w+)*(\.\w{2,3})+$/.test(email)) { + if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { alert("This is not a valid email"); return; } // TODO this has to change to get the edition the invite belongs to - const edition = "2022"; - const payload = new FormData(); - payload.append("username", email); - payload.append("name", name); - payload.append("password", password); - payload.append("confirmPassword", confirmPassword); + const edition = "1"; + const payload: RegisterFields = { email: email, name: name, uuid: uuid, pw: password }; axiosInstance .post("/editions/" + edition + "/register/email", payload) @@ -43,70 +44,84 @@ function RegisterForm() { const navigate = useNavigate(); - return ( -
-
- -

Create an account

-
- Sign up with your social media account or email address -
-
-
- -
- -
-
+ const params = useParams(); + const uuid = params.uuid; -

or

+ const [validUuid, setUuid] = useState(false); -
-
- setEmail(e.target.value)} - /> + axiosInstance.get("/editions/" + 1 + "/invites/" + uuid).then(response => { + if (response.data.uuid === uuid) { + setUuid(true); + } + }); + + if (validUuid && uuid) { + return ( +
+
+

Create an account

+ +
+ Sign up with your social media account or email address. Your unique link is + not useable again ({uuid})
-
- setName(e.target.value)} - /> +
+
+ +
+ +
-
- setPassword(e.target.value)} - /> + +

or

+ +
+
+ setEmail(e.target.value)} + /> +
+
+ setName(e.target.value)} + /> +
+
+ setPassword(e.target.value)} + /> +
+
+ setConfirmPassword(e.target.value)} + /> +
- setConfirmPassword(e.target.value)} - /> +
-
- -
-
- ); + ); + } else return
Not a valid register url
; } -export default RegisterForm; +export default RegisterPage; From 0a93f4e11778ac3cf5d0c9e374dc2b69fbdd1fa1 Mon Sep 17 00:00:00 2001 From: beguille Date: Sat, 26 Mar 2022 17:16:26 +0100 Subject: [PATCH 091/536] small mistake fixed --- backend/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/settings.py b/backend/settings.py index fe64b105f..5390e3e1b 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -29,7 +29,7 @@ """JWT token key""" SECRET_KEY: str = env.str("SECRET_KEY", "4d16a9cc83d74144322e893c879b5f639088c15dc1606b11226abbd7e97f5ee5") -ACCESS_TOKEN_EXPIRE_HOURS: int = env.str("ACCESS_TOKEN_EXPIRE_HOURS", 168) +ACCESS_TOKEN_EXPIRE_HOURS: int = env.int("ACCESS_TOKEN_EXPIRE_HOURS", 168) """Frontend""" FRONTEND_URL: str = env.str("FRONTEND_URL", "http://localhost:3000") From e9c23664ad24fb51f5b49a29a990aabc1e6f3d68 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sat, 26 Mar 2022 17:57:03 +0100 Subject: [PATCH 092/536] frontend code + style guide --- frontend/frontend_guide.md | 462 +++++++++++++++++++++++++++++++++++++ 1 file changed, 462 insertions(+) create mode 100644 frontend/frontend_guide.md diff --git a/frontend/frontend_guide.md b/frontend/frontend_guide.md new file mode 100644 index 000000000..991e3c596 --- /dev/null +++ b/frontend/frontend_guide.md @@ -0,0 +1,462 @@ +# Frontend Guide + +General guidelines & style conventions relating to frontend development. Don't do this, but do that! + +A lot of these guidelines revolve around directory structure and cleaner components. We don't want `.tsx`-files with thousands of `classNames` scattered all over the place, but there are way better solutions as you'll see in this guide. + +## Practical: Making API calls to the backend + +There is a pre-defined `axiosInstance` for you to use, which has things like the base url pre-configured. + +*Note that all logic should go to separate files, so the import path here suggests the file is in `/frontend/src/utils/api/`.* + +Don't do + +```ts +import axios from "axios"; + +async function someApiCall() { + await axios.get("https://sel2-3.ugent.be/api/students", headers={"Authorization": `Bearer ${token}`); +} +``` + +but do + +```ts +import { axiosInstance } from "./api"; + +async function someApiCall() { + // Note that we can now leave out the base url & headers! + // Code is cleaner, shorter, and you can't make mistakes against the url anymore + await axiosInstance.get("/students"); +} +``` + +## Practical: TypeScript interfaces for API responses + +In order to typehint the response of an API call, you can create an `interface`. This will tell TypeScript that certain fields are present, and what their type is, and it allows you to interact with the response as if it would be a class instance. Your IDE will also give intelligent code completion when using these interfaces. + +**Interfaces that are only relevant to one file should be defined at the top of the file, interfaces relevant to multiple files should be moved to another directory**. + +```ts +// /data/interfaces/students.ts + +// This interface can probably be used in multiple places, so it should be in a publicly accessible file +export interface Student { + id: Number; + name: string; + age: Number; +} +``` + +```ts +// /utils/api/students.ts + +// This interface is specific to one API call, no need to expose it to other files +interface GetStudentsResponse { + students: Student[]; // <- Yes, you can use interfaces as types in other interfaces +} + +async function getStudents(): GetStudentsResponse { + const response = await axiosInstance.get("/students"); + + // The "as [interface]"-keyword will tell TypeScript that the response has the structure of [interface] + return response as GetStudentsResponse; +} +``` + +### Note: you only have to add the fields that you care about + +An interface can be seen as a "view" on an object, pulling some fields out. There's no need to create an interface for the entire API response body if you only need one field. + +Of course, if an existing interface already has the field(s) you need, there's no need to make a smaller one just for the sake of leaving some fields out. This only applies to creating **new** interfaces for specific responses. + +In the example below, `Student` has an `id`- and an `age`-field, but neither of them are used. There's no need to include them in the interface. If there would already be a `Student` interface, with **all** fields, you can use the existing interface instead of making a custom view. + +Don't do + +```ts +interface StudentResponse { + id: Number; // <- Unused field + name: string; + age: Number; // <- Unused field + // ... +} + +async function getStudentName(id: Number): string { + const student = (await axiosInstance.get(`/students/${id}`)) as StudentResponse; + return response.name; +} +``` + +but do + +```ts +interface StudentResponse { + name: string; // <- Only field we use +} + +async function getStudentName(id: Number): string { + const student = (await axiosInstance.get(`/students/${id}`)) as StudentResponse; + return response.name; +} +``` + +or, if an interface already exists, do + +```ts +// "Student" already exists and has a lot of fields, "name" is one of those fields +// We don't care about the rest, but it's not necessary to make a new interface +// if the existing one can be used +// (it might even make the code more readable to use the old/generalized interface) +import { Student } from "../../data/interfaces/students"; + +async function getStudentName(id: Number): string { + const student = (await axiosInstance.get(`/students/${id}`)) as Student; + return response.name; +} +``` + + + +## Moving pages to separate directories + +Don't do + +``` +views + - LoginPage.tsx + - HomePage.tsx +``` + +but do + +``` +views + - LoginPage + - LoginPage.tsx + - HomePage + - HomePage.tsx +``` + +## Keep `.css`-files next to the `.tsx`-files they are for + +Don't do + +``` +css-files + - App.css + +views + - App.tsx +``` + +but do + +``` +views + - App + - App.tsx + - App.css +``` + +This keeps the directories clean, so we don't end up with a thousand `.css` files scattered throughout the repository. If a file is next to a component, you can instantly find one if you have the other. + +## Use `react-bootstrap`-components instead of adding unnecessary Bootstrap `className`s + +`react-bootstrap` has a lot of built-in components that do some basic & commonly used functionality for you. It's cleaner to use these components than to make a `
` because it keeps the code more readable. + +Don't do + +```tsx +export default function Component() { + return ( +
+
+ { /* ... */ } +
+
+ ); +} +``` + +but do + +```tsx +import Button from "react-bootstrap/Button"; +import Container from "react-bootstrap/Container"; + +export default function Component() { + return ( + + + + ); +} +``` + +### Note: only import what you need + +Docs on importing components can be found [here](https://react-bootstrap.github.io/getting-started/introduction/#importing-components). + +Don't do + +```tsx +// This pulls the entire library to only use this one component +import { Button } from "react-bootstrap"; +``` + +but do + +```tsx +// This navigates to the location of the component and only imports that +import Button from "react-bootstrap/Button"; +``` + +More info & tutorials about `react-bootstrap`, including a list of all available components, can be found on their [website](https://react-bootstrap.github.io/getting-started/introduction/). + +## Use `styled-components` instead of unnecessary `.css`-files, -classes, and inline styling + +If you create a `.css`-class that you only apply to a single element (or a couple of elements in the same isolated component), it's better to create a `styled-component`. This keeps the `.tsx`-files clean, as there are no unnecessary `className`s on every element. It also makes the code a bit easier to read, as every component now has a name instead of being a `div`. The same goes for inline `style`-tags. **Don't do this**. + +Only create `.css`-files if you really have to. + +The name of this file should be `styles.ts`, and be present next to the `.tsx`-file it's for. If you would somehow end up with multiple `styles` in the same directory, prefix them with the name of the component (`Component.styles.ts`). + +Don't do + +```css +/* Component.css */ +.page-content { + color: red; + background-color: blue; +} + +/* ... */ +``` + +```tsx +// Component.tsx +import "./Component.css" + +export default function Component() { + return ( +
+ // ... More divs with a lot more classNames here +
+ ); +} +``` + +but do + +```ts +// styles.ts +import styled from "styled-components"; + +// You can create a component for every tag there is, +// I'm just using a
in this example here. +// styled.h3`` also works just fine. +export const PageContent = styled.div` + color: red; + background-color: blue; + font-weight: bold; +`; +``` + +```tsx +// Component.tsx +import { PageContent } from "./styles"; + +export default function Component() { + return ( + { /* <- Notice how there are no classNames or inline styles */ } + // ... more styled-components here + + ); +} +``` + +Directory structure: + +``` +components + - SomePage + - Component + - Component.tsx + - Component.css + - styles.ts +``` + +### Note: you can also turn `react-bootstrap`-components into `styled-components` to keep the code even cleaner. + +Combining the previous tip with this one. To create a `styled-component` from a `react-bootstrap`-component (or any other component you have made), by passing the component as an argument to the `styled`-function. + +```ts +// styles.ts +import Container from "react-bootstrap/Container"; +import styled from "styled-components"; + +export const BoldContainer = styled(Container)` + font-weight: bold; +`; +``` + +More info & tutorials on `styled-components` can be found on their [website](https://styled-components.com/docs/basics). + +## Split every page & component down into small components + +We don't want massive `.tsx` files with hundreds of `
`s and 500 indents. Split every independent part down into small components that each have their own directory and isolated `.css` file. + +Don't do + +``` +views + - SomePage + - SomePage.tsx + - SomePage.css +``` + +```tsx +// SomePage.tsx +export default function SomePage() { + return ( +
+
+ // Page header, lots of code here +
+
+ // Page footer, also a lot of code here +
+
+ ); +} +``` + +but do + +``` +components + - SomePage <- Components only related to this page should also go in a separate directory to split them from the rest + - Header + - Header.tsx + - Header.css + - styles.ts + - Footer + - Footer.tsx + - Footer.css + - styles.ts +views + - SomePage + - +``` + +```tsx +// SomePage.tsx +export default function SomePage() { + return ( +
+
+
+
+ ); +} +``` + +```tsx +// Header.tsx +export default function Header() { + return ( +
+ // Either more small components here, or an acceptable amount of code +
+ ); +} +``` + +```tsx +// Footer.tsx +export default function Footer() { + return ( +
+ // Either more small components here, or an acceptable amount of code +
+ ); +} +``` + +## Use `index.ts`-files to re-export components and keep import paths short + +In JavaScript (and, as a result, also TypeScript) `index.(j/t)s` can be used to export specific functions from a module, keeping the rest hidden. + +If a piece of code isn't required outside of its module, don't export it in the `index.ts`-file. This way, only the "public" code is exported (and visible to outsider modules), while local logic is kept local. + +You can create as many `index.ts`-files as you want, which can all re-export nested components, functions, `const` variables, and more. The only rule you should follow is that you shouldn't re-export something to a level where you don't need it anymore. If `/components/Header/Button` is only used in the `Header`, don't include it in the `index.ts`-file from `Header`. This would "include" it in the Header module, even though it's not used outside of it. Keep the component **private** to the module. + +Don't do + +```ts +// This import means "/Button/Button.ts", so we are mentioning Button twice +// even though it's obvious that that's what we're trying to import +import Button from "./Button/Button"; +``` + +but do + +```ts +// /Button/index.ts +export { default as Button } from "./Button"; +``` + +```tsx +// The "/Button/index.ts"-file allows us to import from the name of the MODULE +// rather than going all the way to the ".ts(x)"-file +import { Button } from "./Button"; +``` + +Directory structure: + +``` +components + - Footer + - Button + - Button.tsx + - index.ts // Re-exports the Button: export { default as Button } from "./Button"; + - index.ts // Re-exports the Footer: export { default as Footer } from "./Footer"; +``` + +## Move logic to separate files + +Just as we did in the backend, the main components don't need to handle the logic that they execute. They merely call functions defined elsewhere. + +Don't do + +```tsx +// Component.tsx +import { axiosInstance } from "../utils/api"; + +export default function Component() { + return ( + +
+ ); + } + let error = null; + if (this.state.errorMessage) { + error =
{this.state.errorMessage}
; + } + let link = null; + if (this.state.link) { + link =
{this.state.link}
; + } + return ( +
+
+ this.setEmail(e.target.value)} + /> + {button} +
+ {error} + {link} +
+ ); + } +} + +function UsersPage() { + return ; +} + +export default UsersPage; From 4540805db32441e859f26195ac335bb09b528ba5 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sat, 26 Mar 2022 21:18:12 +0100 Subject: [PATCH 095/536] hopefully fix wonky indents --- frontend/frontend_guide.md | 77 +++++++++++++++----------------------- 1 file changed, 30 insertions(+), 47 deletions(-) diff --git a/frontend/frontend_guide.md b/frontend/frontend_guide.md index dceb6051b..27cad213b 100644 --- a/frontend/frontend_guide.md +++ b/frontend/frontend_guide.md @@ -8,7 +8,7 @@ A lot of these guidelines revolve around directory structure and cleaner compone There is a pre-defined `axiosInstance` for you to use, which has things like the base url pre-configured. -*Note that all logic should go to separate files, so the import path here suggests the file is in `/frontend/src/utils/api/`.* +_Note that all logic should go to separate files, so the import path here suggests the file is in `/frontend/src/utils/api/`._ Don't do @@ -45,7 +45,7 @@ In order to typehint the response of an API call, you can create an `interface`. export interface Student { id: Number; name: string; - age: Number; + age: Number; } ``` @@ -59,7 +59,7 @@ interface GetStudentsResponse { async function getStudents(): GetStudentsResponse { const response = await axiosInstance.get("/students"); - + // The "as [interface]"-keyword will tell TypeScript that the response has the structure of [interface] return response as GetStudentsResponse; } @@ -77,14 +77,14 @@ Don't do ```ts interface StudentResponse { - id: Number; // <- Unused field + id: Number; // <- Unused field name: string; - age: Number; // <- Unused field + age: Number; // <- Unused field // ... } async function getStudentName(id: Number): string { - const student = (await axiosInstance.get(`/students/${id}`)) as StudentResponse; + const student = (await axiosInstance.get(`/students/${id}`)) as StudentResponse; return response.name; } ``` @@ -93,11 +93,11 @@ but do ```ts interface StudentResponse { - name: string; // <- Only field we use + name: string; // <- Only field we use } async function getStudentName(id: Number): string { - const student = (await axiosInstance.get(`/students/${id}`)) as StudentResponse; + const student = (await axiosInstance.get(`/students/${id}`)) as StudentResponse; return response.name; } ``` @@ -112,13 +112,11 @@ or, if an interface already exists, do import { Student } from "../../data/interfaces/students"; async function getStudentName(id: Number): string { - const student = (await axiosInstance.get(`/students/${id}`)) as Student; + const student = (await axiosInstance.get(`/students/${id}`)) as Student; return response.name; } ``` - - ## Moving pages to separate directories Don't do @@ -170,13 +168,11 @@ Don't do ```tsx export default function Component() { - return ( -
-
- { /* ... */ } -
-
- ); + return ( +
+
{/* ... */}
+
+ ); } ``` @@ -189,9 +185,7 @@ import Container from "react-bootstrap/Container"; export default function Component() { return ( - + ); } @@ -260,9 +254,9 @@ import styled from "styled-components"; // I'm just using a
in this example here. // styled.h3`` also works just fine. export const PageContent = styled.div` - color: red; - background-color: blue; - font-weight: bold; + color: red; + background-color: blue; + font-weight: bold; `; ``` @@ -272,8 +266,10 @@ import { PageContent } from "./styles"; export default function Component() { return ( - { /* <- Notice how there are no classNames or inline styles */ } - // ... more styled-components here + + {" "} + {/* <- Notice how there are no classNames or inline styles */} + // ... more styled-components here ); } @@ -300,7 +296,7 @@ import Container from "react-bootstrap/Container"; import styled from "styled-components"; export const BoldContainer = styled(Container)` - font-weight: bold; + font-weight: bold; `; ``` @@ -323,13 +319,9 @@ views // SomePage.tsx export default function SomePage() { return ( -
-
- // Page header, lots of code here -
-
- // Page footer, also a lot of code here -
+
+
// Page header, lots of code here
+
// Page footer, also a lot of code here
); } @@ -358,8 +350,8 @@ views // SomePage.tsx export default function SomePage() { return ( -
-
+
+
); @@ -369,22 +361,14 @@ export default function SomePage() { ```tsx // Header.tsx export default function Header() { - return ( -
- // Either more small components here, or an acceptable amount of code -
- ); + return
// Either more small components here, or an acceptable amount of code
; } ``` ```tsx // Footer.tsx export default function Footer() { - return ( -
- // Either more small components here, or an acceptable amount of code -
- ); + return
// Either more small components here, or an acceptable amount of code
; } ``` @@ -460,4 +444,3 @@ export async function getStudents() { // src/utils/api/index.ts export { getStudents } from "./students"; ``` - From 3476d438fb290772821a3ad03cc85678723d0466 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sat, 26 Mar 2022 21:30:02 +0100 Subject: [PATCH 096/536] fix more typo --- frontend/frontend_guide.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/frontend_guide.md b/frontend/frontend_guide.md index 27cad213b..3599b4cb9 100644 --- a/frontend/frontend_guide.md +++ b/frontend/frontend_guide.md @@ -215,6 +215,8 @@ More info & tutorials about `react-bootstrap`, including a list of all available If you create a `.css`-class that you only apply to a single element (or a couple of elements in the same isolated component), it's better to create a `styled-component`. This keeps the `.tsx`-files clean, as there are no unnecessary `className`s on every element. It also makes the code a bit easier to read, as every component now has a name instead of being a `div`. The same goes for inline `style`-tags. **Don't do this**. +As you'll hopefully see in the example below, the resulting code is a lot calmer and less chaotic. + Only create `.css`-files if you really have to. The name of this file should be `styles.ts`, and be present next to the `.tsx`-file it's for. If you would somehow end up with multiple `styles` in the same directory, prefix them with the name of the component (`Component.styles.ts`). @@ -238,7 +240,7 @@ import "./Component.css" export default function Component() { return (
- // ... More divs with a lot more classNames here + { /* ... More divs with a lot more classNames here */ }
); } @@ -266,10 +268,9 @@ import { PageContent } from "./styles"; export default function Component() { return ( + {/* Notice how there are no classNames or inline styles, the code is a lot less hectic */} - {" "} - {/* <- Notice how there are no classNames or inline styles */} - // ... more styled-components here + { /* more styled-components here */ } ); } From 2295020132617f15aa5901099d2ba49918c6b663 Mon Sep 17 00:00:00 2001 From: beguille Date: Sun, 27 Mar 2022 10:30:20 +0200 Subject: [PATCH 097/536] closes #124, moved CamelCaseModel to utils.py --- backend/src/app/schemas/editions.py | 2 +- backend/src/app/schemas/invites.py | 2 +- backend/src/app/schemas/login.py | 2 +- backend/src/app/schemas/projects.py | 2 +- backend/src/app/schemas/skills.py | 2 +- backend/src/app/schemas/users.py | 3 +-- backend/src/app/schemas/utils.py | 16 ++++++++++++++++ backend/src/app/schemas/webhooks.py | 14 +------------- 8 files changed, 23 insertions(+), 20 deletions(-) create mode 100644 backend/src/app/schemas/utils.py diff --git a/backend/src/app/schemas/editions.py b/backend/src/app/schemas/editions.py index e5a5dae19..63d64ba0e 100644 --- a/backend/src/app/schemas/editions.py +++ b/backend/src/app/schemas/editions.py @@ -1,4 +1,4 @@ -from src.app.schemas.webhooks import CamelCaseModel +from src.app.schemas.utils import CamelCaseModel class EditionBase(CamelCaseModel): diff --git a/backend/src/app/schemas/invites.py b/backend/src/app/schemas/invites.py index 9f616bfe4..1ce058d0e 100644 --- a/backend/src/app/schemas/invites.py +++ b/backend/src/app/schemas/invites.py @@ -3,7 +3,7 @@ from pydantic import Field, validator from src.app.schemas.validators import validate_email_format -from src.app.schemas.webhooks import CamelCaseModel +from src.app.schemas.utils import CamelCaseModel class EmailAddress(CamelCaseModel): diff --git a/backend/src/app/schemas/login.py b/backend/src/app/schemas/login.py index 1c056a30d..79406c8dd 100644 --- a/backend/src/app/schemas/login.py +++ b/backend/src/app/schemas/login.py @@ -1,4 +1,4 @@ -from src.app.schemas.webhooks import CamelCaseModel +from src.app.schemas.utils import CamelCaseModel class Token(CamelCaseModel): diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index 42c6a3362..fe8e0d83b 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -1,6 +1,6 @@ from pydantic import BaseModel -from src.app.schemas.webhooks import CamelCaseModel +from src.app.schemas.utils import CamelCaseModel from src.database.enums import DecisionEnum diff --git a/backend/src/app/schemas/skills.py b/backend/src/app/schemas/skills.py index 69dd0d207..9c6e73afc 100644 --- a/backend/src/app/schemas/skills.py +++ b/backend/src/app/schemas/skills.py @@ -1,4 +1,4 @@ -from src.app.schemas.webhooks import CamelCaseModel +from src.app.schemas.utils import CamelCaseModel class SkillBase(CamelCaseModel): diff --git a/backend/src/app/schemas/users.py b/backend/src/app/schemas/users.py index 07b1aef21..3d3c35cd3 100644 --- a/backend/src/app/schemas/users.py +++ b/backend/src/app/schemas/users.py @@ -1,5 +1,4 @@ - -from src.app.schemas.webhooks import CamelCaseModel +from src.app.schemas.utils import CamelCaseModel class User(CamelCaseModel): diff --git a/backend/src/app/schemas/utils.py b/backend/src/app/schemas/utils.py new file mode 100644 index 000000000..9769656b7 --- /dev/null +++ b/backend/src/app/schemas/utils.py @@ -0,0 +1,16 @@ +from humps import camelize +from pydantic import BaseModel + + +def to_camel(string: str) -> str: + """Return the camel case version of a given string""" + return camelize(string) + + +class CamelCaseModel(BaseModel): + """Base model that converts snake to camel case when serialized""" + + class Config: + """Config""" + alias_generator = to_camel + allow_population_by_field_name = True diff --git a/backend/src/app/schemas/webhooks.py b/backend/src/app/schemas/webhooks.py index 2763da41d..1d8372c4c 100644 --- a/backend/src/app/schemas/webhooks.py +++ b/backend/src/app/schemas/webhooks.py @@ -1,21 +1,9 @@ from typing import Optional from uuid import UUID -from humps import camelize from pydantic import BaseModel - -def to_camel(string: str) -> str: - """Return the camel case version of a given string""" - return camelize(string) - - -class CamelCaseModel(BaseModel): - """Base model that converts snake to camel case when serialized""" - class Config: - """Config""" - alias_generator = to_camel - allow_population_by_field_name = True +from src.app.schemas.utils import CamelCaseModel class QuestionOption(CamelCaseModel): From a43d41c1c7eaad94c5ac3483106b77acbeb3ad1f Mon Sep 17 00:00:00 2001 From: beguille Date: Sun, 27 Mar 2022 10:34:51 +0200 Subject: [PATCH 098/536] closes #124, moved CamelCaseModel to utils.py --- backend/src/database/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/database/models.py b/backend/src/database/models.py index 0ea5073cf..8e9370798 100644 --- a/backend/src/database/models.py +++ b/backend/src/database/models.py @@ -28,6 +28,7 @@ class AuthEmail(Base): email_auth_id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.user_id"), nullable=False) + email = Column(Text, unique=True, nullable=False) pw_hash = Column(Text, nullable=False) user: User = relationship("User", back_populates="email_auth", uselist=False) @@ -39,6 +40,7 @@ class AuthGitHub(Base): gh_auth_id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.user_id"), nullable=False) + email = Column(Text, unique=True, nullable=False) user: User = relationship("User", back_populates="github_auth", uselist=False) @@ -49,6 +51,7 @@ class AuthGoogle(Base): google_auth_id = Column(Integer, primary_key=True) 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) @@ -290,7 +293,6 @@ class User(Base): user_id = Column(Integer, primary_key=True) name = Column(Text, nullable=False) - email = Column(Text, unique=True, nullable=False) admin = Column(Boolean, nullable=False, default=False) coach_request: CoachRequest = relationship("CoachRequest", back_populates="user", uselist=False) From f20f512593d22f2489badfe12083297caf10ad15 Mon Sep 17 00:00:00 2001 From: beguille Date: Sun, 27 Mar 2022 10:39:41 +0200 Subject: [PATCH 099/536] Revert "closes #124, moved CamelCaseModel to utils.py" This reverts commit a43d41c1c7eaad94c5ac3483106b77acbeb3ad1f. --- backend/src/database/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/src/database/models.py b/backend/src/database/models.py index 8e9370798..0ea5073cf 100644 --- a/backend/src/database/models.py +++ b/backend/src/database/models.py @@ -28,7 +28,6 @@ class AuthEmail(Base): email_auth_id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.user_id"), nullable=False) - email = Column(Text, unique=True, nullable=False) pw_hash = Column(Text, nullable=False) user: User = relationship("User", back_populates="email_auth", uselist=False) @@ -40,7 +39,6 @@ class AuthGitHub(Base): gh_auth_id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.user_id"), nullable=False) - email = Column(Text, unique=True, nullable=False) user: User = relationship("User", back_populates="github_auth", uselist=False) @@ -51,7 +49,6 @@ class AuthGoogle(Base): google_auth_id = Column(Integer, primary_key=True) 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) @@ -293,6 +290,7 @@ class User(Base): user_id = Column(Integer, primary_key=True) name = Column(Text, nullable=False) + email = Column(Text, unique=True, nullable=False) admin = Column(Boolean, nullable=False, default=False) coach_request: CoachRequest = relationship("CoachRequest", back_populates="user", uselist=False) From 6fcd6143f94aaf9501ccc436dfcf7bfc79597af4 Mon Sep 17 00:00:00 2001 From: beguille Date: Sun, 27 Mar 2022 13:40:55 +0200 Subject: [PATCH 100/536] moved email from User to AuthEmail, two tests are still failing --- .../ca4c4182b93a_moved_email_to_authemail.py | 40 +++++++++++++++++++ backend/src/app/logic/register.py | 7 +++- backend/src/app/logic/security.py | 4 +- backend/src/app/schemas/projects.py | 1 - backend/src/app/schemas/register.py | 5 ++- backend/src/app/schemas/users.py | 1 - backend/src/database/crud/register.py | 13 ++++-- backend/src/database/models.py | 2 +- backend/tests/fill_database.py | 16 ++++---- .../test_database/test_crud/test_projects.py | 2 +- .../test_crud/test_projects_students.py | 2 +- .../test_database/test_crud/test_register.py | 11 +++-- .../test_database/test_crud/test_users.py | 22 +++++----- backend/tests/test_database/test_models.py | 4 +- backend/tests/test_logic/test_register.py | 1 - .../test_projects/test_projects.py | 5 +-- .../test_students/test_students.py | 2 +- .../test_register/test_register.py | 11 ++--- .../test_routers/test_login/test_login.py | 8 ++-- .../test_routers/test_skills/test_skills.py | 4 +- .../test_routers/test_users/test_users.py | 22 +++++----- .../tests/utils/authorization/auth_client.py | 6 +-- 22 files changed, 117 insertions(+), 72 deletions(-) create mode 100644 backend/migrations/versions/ca4c4182b93a_moved_email_to_authemail.py diff --git a/backend/migrations/versions/ca4c4182b93a_moved_email_to_authemail.py b/backend/migrations/versions/ca4c4182b93a_moved_email_to_authemail.py new file mode 100644 index 000000000..9973803f7 --- /dev/null +++ b/backend/migrations/versions/ca4c4182b93a_moved_email_to_authemail.py @@ -0,0 +1,40 @@ +"""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/src/app/logic/register.py b/backend/src/app/logic/register.py index c3202a338..ad3b5ae6f 100644 --- a/backend/src/app/logic/register.py +++ b/backend/src/app/logic/register.py @@ -1,3 +1,6 @@ +import sqlite3 + +import sqlalchemy.exc from sqlalchemy.orm import Session from src.app.schemas.register import NewUser @@ -13,8 +16,8 @@ def create_request(db: Session, new_user: NewUser, edition: Edition) -> None: transaction = db.begin_nested() invite_link: InviteLink = get_invite_link_by_uuid(db, new_user.uuid) try: - user = create_user(db, new_user.name, new_user.email) - create_auth_email(db, user, get_password_hash(new_user.pw)) + user = create_user(db, new_user.name) + create_auth_email(db, user, get_password_hash(new_user.pw), new_user.email) create_coach_request(db, user, edition) delete_invite_link(db, invite_link) except Exception as exception: diff --git a/backend/src/app/logic/security.py b/backend/src/app/logic/security.py index 32aed5bfb..4564d9ac9 100644 --- a/backend/src/app/logic/security.py +++ b/backend/src/app/logic/security.py @@ -10,7 +10,6 @@ # Configuration ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_HOURS = 24 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") @@ -43,7 +42,8 @@ def get_password_hash(password: str) -> str: # TODO remove this when the users crud has been implemented def get_user_by_email(db: Session, email: str) -> models.User: """Find a user by their email address""" - return db.query(models.User).where(models.User.email == email).one() + auth_email = db.query(models.AuthEmail).where(models.AuthEmail.email == email).one() + return db.query(models.User).where(models.User.user_id == auth_email.user_id).one() # TODO remove this when the users crud has been implemented diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index fe8e0d83b..8deac22e4 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -8,7 +8,6 @@ class User(CamelCaseModel): """Represents a User from the database""" user_id: int name: str - email: str class Config: orm_mode = True diff --git a/backend/src/app/schemas/register.py b/backend/src/app/schemas/register.py index a262d478a..59eaeb8b0 100644 --- a/backend/src/app/schemas/register.py +++ b/backend/src/app/schemas/register.py @@ -3,7 +3,10 @@ class NewUser(EmailAddress): - """The scheme of a new user""" + """ + The scheme of a new user + The email address will be stored in AuthEmail, but is included here to easily create a user + """ name: str pw: str uuid: UUID diff --git a/backend/src/app/schemas/users.py b/backend/src/app/schemas/users.py index 3d3c35cd3..ecd7a1361 100644 --- a/backend/src/app/schemas/users.py +++ b/backend/src/app/schemas/users.py @@ -6,7 +6,6 @@ class User(CamelCaseModel): user_id: int name: str - email: str admin: bool class Config: diff --git a/backend/src/database/crud/register.py b/backend/src/database/crud/register.py index 60fda3ea7..e4936e088 100644 --- a/backend/src/database/crud/register.py +++ b/backend/src/database/crud/register.py @@ -1,15 +1,18 @@ +import sqlalchemy.exc from sqlalchemy.orm import Session +from src.app.exceptions.register import FailedToAddNewUserException from src.database.models import AuthEmail, CoachRequest, User, Edition -def create_user(db: Session, name: str, email: str) -> User: +def create_user(db: Session, name: str) -> User: """Create a user""" - new_user: User = User(name=name, email=email) + new_user: User = User(name=name) db.add(new_user) db.commit() return new_user + def create_coach_request(db: Session, user: User, edition: Edition) -> CoachRequest: """Create a coach request""" coach_request: CoachRequest = CoachRequest(user=user, edition=edition) @@ -17,9 +20,11 @@ def create_coach_request(db: Session, user: User, edition: Edition) -> CoachRequ db.commit() return coach_request -def create_auth_email(db: Session, user: User, pw_hash: str) -> AuthEmail: + +def create_auth_email(db: Session, user: User, pw_hash: str, email: str) -> AuthEmail: """Create a authentication for email""" - auth_email : AuthEmail = AuthEmail(user=user, pw_hash = pw_hash) + print("called") + auth_email: AuthEmail = AuthEmail(user=user, pw_hash=pw_hash, email=email) db.add(auth_email) db.commit() return auth_email diff --git a/backend/src/database/models.py b/backend/src/database/models.py index 0ea5073cf..935ac5154 100644 --- a/backend/src/database/models.py +++ b/backend/src/database/models.py @@ -28,6 +28,7 @@ class AuthEmail(Base): email_auth_id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.user_id"), nullable=False) + email = Column(Text, unique=True, nullable=False) pw_hash = Column(Text, nullable=False) user: User = relationship("User", back_populates="email_auth", uselist=False) @@ -290,7 +291,6 @@ class User(Base): user_id = Column(Integer, primary_key=True) name = Column(Text, nullable=False) - email = Column(Text, unique=True, nullable=False) admin = Column(Boolean, nullable=False, default=False) coach_request: CoachRequest = relationship("CoachRequest", back_populates="user", uselist=False) diff --git a/backend/tests/fill_database.py b/backend/tests/fill_database.py index 37dc40d7c..22d816403 100644 --- a/backend/tests/fill_database.py +++ b/backend/tests/fill_database.py @@ -16,10 +16,10 @@ def fill_database(db: Session): db.commit() # Users - admin: User = User(name="admin", email="admin@ngmail.com", admin=True) - coach1: User = User(name="coach1", email="coach1@noutlook.be") - coach2: User = User(name="coach2", email="coach2@noutlook.be") - request: User = User(name="request", email="request@ngmail.com") + admin: User = User(name="admin", admin=True) + coach1: User = User(name="coach1") + coach2: User = User(name="coach2") + request: User = User(name="request") db.add(admin) db.add(coach1) db.add(coach2) @@ -28,10 +28,10 @@ def fill_database(db: Session): # AuthEmail pw_hash = get_password_hash("wachtwoord") - auth_email_admin: AuthEmail = AuthEmail(user=admin, pw_hash=pw_hash) - auth_email_coach1: AuthEmail = AuthEmail(user=coach1, pw_hash=pw_hash) - auth_email_coach2: AuthEmail = AuthEmail(user=coach2, pw_hash=pw_hash) - auth_email_request: AuthEmail = AuthEmail(user=request, pw_hash=pw_hash) + auth_email_admin: AuthEmail = AuthEmail(user=admin, email="admin@ngmail.com", pw_hash=pw_hash) + auth_email_coach1: AuthEmail = AuthEmail(user=coach1, email="coach1@noutlook.be", pw_hash=pw_hash) + auth_email_coach2: AuthEmail = AuthEmail(user=coach2, email="coach2@noutlook.be", pw_hash=pw_hash) + auth_email_request: AuthEmail = AuthEmail(user=request, email="request@ngmail.com", pw_hash=pw_hash) db.add(auth_email_admin) db.add(auth_email_coach1) db.add(auth_email_coach2) diff --git a/backend/tests/test_database/test_crud/test_projects.py b/backend/tests/test_database/test_crud/test_projects.py index 41d6f2068..b799598c7 100644 --- a/backend/tests/test_database/test_crud/test_projects.py +++ b/backend/tests/test_database/test_crud/test_projects.py @@ -20,7 +20,7 @@ def database_with_data(database_session: Session) -> Session: database_session.add(project1) database_session.add(project2) database_session.add(project3) - user: User = User(name="coach1", email="user@user.be") + 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") 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 44fa96a05..b43bfbf41 100644 --- a/backend/tests/test_database/test_crud/test_projects_students.py +++ b/backend/tests/test_database/test_crud/test_projects_students.py @@ -18,7 +18,7 @@ def database_with_data(database_session: Session) -> Session: database_session.add(project1) database_session.add(project2) database_session.add(project3) - user: User = User(name="coach1", email="user@user.be") + 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") diff --git a/backend/tests/test_database/test_crud/test_register.py b/backend/tests/test_database/test_crud/test_register.py index a12c713c9..2185d9eaa 100644 --- a/backend/tests/test_database/test_crud/test_register.py +++ b/backend/tests/test_database/test_crud/test_register.py @@ -6,19 +6,18 @@ def test_create_user(database_session: Session): """Tests for creating a user""" - create_user(database_session, "jos", "mail@email.com") + create_user(database_session, "jos") a = database_session.query(User).where(User.name == "jos").all() assert len(a) == 1 assert a[0].name == "jos" - assert a[0].email == "mail@email.com" def test_react_coach_request(database_session: Session): """Tests for creating a coach request""" - edition = Edition(year = 2022) + edition = Edition(year=2022) database_session.add(edition) database_session.commit() - u = create_user(database_session, "jos", "mail@email.com") + u = create_user(database_session, "jos") create_coach_request(database_session, u, edition) a = database_session.query(CoachRequest).where(CoachRequest.user == u).all() @@ -29,8 +28,8 @@ def test_react_coach_request(database_session: Session): def test_create_auth_email(database_session: Session): """Tests for creating a auth email""" - u = create_user(database_session, "jos", "mail@email.com") - create_auth_email(database_session, u, "wachtwoord") + u = create_user(database_session, "jos") + create_auth_email(database_session, u, "wachtwoord", "mail@email.com") a = database_session.query(AuthEmail).where(AuthEmail.user == u).all() diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index 69e3b274b..02f226104 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -11,10 +11,10 @@ def data(database_session: Session) -> dict[str, int]: """Fill database with dummy data""" # Create users - user1 = models.User(name="user1", email="user1@mail.com", admin=True) + user1 = models.User(name="user1", admin=True) database_session.add(user1) - user2 = models.User(name="user2", email="user2@mail.com", admin=False) + user2 = models.User(name="user2", admin=False) database_session.add(user2) # Create editions @@ -90,7 +90,7 @@ def test_edit_admin_status(database_session: Session): """Test changing the admin status of a user""" # Create user - user = models.User(name="user1", email="user1@mail.com", admin=False) + user = models.User(name="user1", admin=False) database_session.add(user) database_session.commit() @@ -105,7 +105,7 @@ def test_add_coach(database_session: Session): """Test adding a user as coach""" # Create user - user = models.User(name="user1", email="user1@mail.com", admin=False) + user = models.User(name="user1", admin=False) database_session.add(user) # Create edition @@ -124,7 +124,7 @@ def test_remove_coach(database_session: Session): """Test removing a user as coach""" # Create user - user = models.User(name="user1", email="user1@mail.com", admin=False) + user = models.User(name="user1", admin=False) database_session.add(user) # Create edition @@ -146,8 +146,8 @@ def test_get_all_requests(database_session: Session): """Test get request for all userrequests""" # Create user - user1 = models.User(name="user1", email="user1@mail.com") - user2 = models.User(name="user2", email="user2@mail.com") + user1 = models.User(name="user1") + user2 = models.User(name="user2") database_session.add(user1) database_session.add(user2) @@ -180,8 +180,8 @@ def test_get_all_requests_from_edition(database_session: Session): """Test get request for all userrequests of a given edition""" # Create user - user1 = models.User(name="user1", email="user1@mail.com") - user2 = models.User(name="user2", email="user2@mail.com") + user1 = models.User(name="user1") + user2 = models.User(name="user2") database_session.add(user1) database_session.add(user2) @@ -214,7 +214,7 @@ def test_accept_request(database_session: Session): """Test accepting a coach request""" # Create user - user1 = models.User(name="user1", email="user1@mail.com") + user1 = models.User(name="user1") database_session.add(user1) # Create edition @@ -241,7 +241,7 @@ def test_reject_request_new_user(database_session: Session): """Test rejecting a coach request""" # Create user - user1 = models.User(name="user1", email="user1@mail.com") + user1 = models.User(name="user1") database_session.add(user1) # Create edition diff --git a/backend/tests/test_database/test_models.py b/backend/tests/test_database/test_models.py index 2a177bc76..230312d82 100644 --- a/backend/tests/test_database/test_models.py +++ b/backend/tests/test_database/test_models.py @@ -9,7 +9,7 @@ def test_user_coach_request(database_session: Session): database_session.commit() # Passing as user_id - user = models.User(name="name", email="email1") + user = models.User(name="name") database_session.add(user) database_session.commit() @@ -20,7 +20,7 @@ def test_user_coach_request(database_session: Session): assert req.user == user # Check if passing as user instead of user_id works - user = models.User(name="name", email="email2") + user = models.User(name="name") database_session.add(user) database_session.commit() diff --git a/backend/tests/test_logic/test_register.py b/backend/tests/test_logic/test_register.py index e58f1f0e5..a3953b3d0 100644 --- a/backend/tests/test_logic/test_register.py +++ b/backend/tests/test_logic/test_register.py @@ -9,7 +9,6 @@ from src.app.exceptions.register import FailedToAddNewUserException - def test_create_request(database_session: Session): """Tests if a normal request can be created""" edition = Edition(year=2022) 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 3bbfaf021..272f99cc4 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 @@ -17,7 +17,7 @@ def database_with_data(database_session: Session) -> Session: database_session.add(project1) database_session.add(project2) database_session.add(project3) - user: User = User(name="coach1", email="user@user.be") + 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") @@ -67,7 +67,6 @@ def test_get_project(database_with_data: Session, test_client: TestClient): response = test_client.get("/editions/1/projects/1") assert response.status_code == status.HTTP_200_OK json = response.json() - print(json) assert json['name'] == 'project1' @@ -218,7 +217,6 @@ def test_patch_project_non_existing_skills(database_with_data: Session, test_cli response = test_client.get("/editions/1/projects/1") json = response.json() - print(json) assert 100 not in json["skills"] @@ -234,7 +232,6 @@ def test_patch_project_non_existing_coach(database_with_data: Session, test_clie assert response.status_code == status.HTTP_404_NOT_FOUND response = test_client.get("/editions/1/projects/1") json = response.json() - print(json) assert 10 not in json["coaches"] 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 1dcd3991e..265222430 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 @@ -17,7 +17,7 @@ def database_with_data(database_session: Session) -> Session: database_session.add(project1) database_session.add(project2) database_session.add(project3) - user: User = User(name="coach1", email="user@user.be") + 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") 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 aa167443b..9176c3488 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 @@ -3,7 +3,7 @@ from starlette.testclient import TestClient -from src.database.models import Edition, InviteLink, User +from src.database.models import Edition, InviteLink, User, AuthEmail def test_ok(database_session: Session, test_client: TestClient): @@ -19,11 +19,12 @@ def test_ok(database_session: Session, test_client: TestClient): "uuid": str(invite_link.uuid)}) assert response.status_code == status.HTTP_201_CREATED user: User = database_session.query(User).where( - User.email == "jw@gmail.com").one() - assert user.name == "Joskes vermeulen" + 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_multipli_times(database_session: Session, test_client: TestClient): +def test_use_uuid_multiple_times(database_session: Session, test_client: TestClient): """Tests that you can't use the same UUID multiple times""" edition: Edition = Edition(year=2022) invite_link: InviteLink = InviteLink( @@ -50,7 +51,7 @@ def test_no_valid_uuid(database_session: Session, test_client: TestClient): "uuid": "550e8400-e29b-41d4-a716-446655440000"}) assert response.status_code == status.HTTP_404_NOT_FOUND users: list[User] = database_session.query( - User).where(User.email == "jw@gmail.com").all() + User).where(User.name == "Joskes vermeulen").all() assert len(users) == 0 diff --git a/backend/tests/test_routers/test_login/test_login.py b/backend/tests/test_routers/test_login/test_login.py index 895ce3d8a..b67d14c81 100644 --- a/backend/tests/test_routers/test_login/test_login.py +++ b/backend/tests/test_routers/test_login/test_login.py @@ -20,12 +20,12 @@ def test_login_existing(database_session: Session, test_client: TestClient): password = "password" # Create new user & auth entries in db - user = User(name="test", email=email) + user = User(name="test") database_session.add(user) database_session.commit() - auth = AuthEmail(pw_hash=security.get_password_hash(password)) + auth = AuthEmail(pw_hash=security.get_password_hash(password), email=email) auth.user = user database_session.add(auth) database_session.commit() @@ -44,12 +44,12 @@ def test_login_existing_wrong_credentials(database_session: Session, test_client password = "password" # Create new user & auth entries in db - user = User(name="test", email=email) + user = User(name="test") database_session.add(user) database_session.commit() - auth = AuthEmail(pw_hash=security.get_password_hash(password)) + auth = AuthEmail(pw_hash=security.get_password_hash(password), email=email) auth.user = user database_session.add(auth) database_session.commit() diff --git a/backend/tests/test_routers/test_skills/test_skills.py b/backend/tests/test_routers/test_skills/test_skills.py index bc9b51b06..326fd08be 100644 --- a/backend/tests/test_routers/test_skills/test_skills.py +++ b/backend/tests/test_routers/test_skills/test_skills.py @@ -14,7 +14,7 @@ def test_get_skills(database_session: Session, auth_client: AuthClient): auth_client (AuthClient): a client used to do rest calls """ auth_client.admin() - skill = Skill(name="Backend", description = "Must know react") + skill = Skill(name="Backend", description="Must know react") database_session.add(skill) database_session.commit() @@ -52,7 +52,7 @@ def test_delete_skill(database_session: Session, auth_client: AuthClient): """ auth_client.admin() - skill = Skill(name="Backend", description = "Must know react") + skill = Skill(name="Backend", description="Must know react") database_session.add(skill) database_session.commit() database_session.refresh(skill) diff --git a/backend/tests/test_routers/test_users/test_users.py b/backend/tests/test_routers/test_users/test_users.py index 7191e968c..febf9ccf4 100644 --- a/backend/tests/test_routers/test_users/test_users.py +++ b/backend/tests/test_routers/test_users/test_users.py @@ -15,10 +15,10 @@ def data(database_session: Session) -> dict[str, str | int]: """Fill database with dummy data""" # Create users - user1 = models.User(name="user1", email="user1@mail.com", admin=True) + user1 = models.User(name="user1", admin=True) database_session.add(user1) - user2 = models.User(name="user2", email="user2@mail.com", admin=False) + user2 = models.User(name="user2", admin=False) database_session.add(user2) # Create editions @@ -106,7 +106,7 @@ def test_edit_admin_status(database_session: Session, auth_client: AuthClient): """Test endpoint for editing the admin status of a user""" auth_client.admin() # Create user - user = models.User(name="user1", email="user1@mail.com", admin=False) + user = models.User(name="user1", admin=False) database_session.add(user) database_session.commit() @@ -125,7 +125,7 @@ def test_add_coach(database_session: Session, auth_client: AuthClient): """Test endpoint for adding coaches""" auth_client.admin() # Create user - user = models.User(name="user1", email="user1@mail.com", admin=False) + user = models.User(name="user1", admin=False) database_session.add(user) # Create edition @@ -146,7 +146,7 @@ def test_remove_coach(database_session: Session, auth_client: AuthClient): """Test endpoint for removing coaches""" auth_client.admin() # Create user - user = models.User(name="user1", email="user1@mail.com") + user = models.User(name="user1") database_session.add(user) # Create edition @@ -173,8 +173,8 @@ def test_get_all_requests(database_session: Session, auth_client: AuthClient): auth_client.admin() # Create user - user1 = models.User(name="user1", email="user1@mail.com") - user2 = models.User(name="user2", email="user2@mail.com") + user1 = models.User(name="user1") + user2 = models.User(name="user2") database_session.add(user1) database_session.add(user2) @@ -207,8 +207,8 @@ def test_get_all_requests_from_edition(database_session: Session, auth_client: A auth_client.admin() # Create user - user1 = models.User(name="user1", email="user1@mail.com") - user2 = models.User(name="user2", email="user2@mail.com") + user1 = models.User(name="user1") + user2 = models.User(name="user2") database_session.add(user1) database_session.add(user2) @@ -245,7 +245,7 @@ def test_accept_request(database_session, auth_client: AuthClient): """Test endpoint for accepting a coach request""" auth_client.admin() # Create user - user1 = models.User(name="user1", email="user1@mail.com") + user1 = models.User(name="user1") database_session.add(user1) # Create edition @@ -271,7 +271,7 @@ def test_reject_request(database_session, auth_client: AuthClient): """Test endpoint for rejecting a coach request""" auth_client.admin() # Create user - user1 = models.User(name="user1", email="user1@mail.com") + user1 = models.User(name="user1") database_session.add(user1) # Create edition diff --git a/backend/tests/utils/authorization/auth_client.py b/backend/tests/utils/authorization/auth_client.py index 2b120c772..65bb216a6 100644 --- a/backend/tests/utils/authorization/auth_client.py +++ b/backend/tests/utils/authorization/auth_client.py @@ -29,7 +29,7 @@ def invalid(self): def admin(self): """Sign in as an admin for all future requests""" # Create a new user in the db - admin = User(name="Pytest Admin", email="admin@pytest.email", admin=True) + admin = User(name="Pytest Admin", admin=True) self.session.add(admin) self.session.commit() @@ -40,7 +40,7 @@ def coach(self, edition: Edition): Assigns the coach to the edition """ # Create a new user in the db - coach = User(name="Pytest Coach", email="coach@pytest.email", admin=False) + coach = User(name="Pytest Coach", admin=False) # Link the coach to the edition coach.editions.append(edition) @@ -53,7 +53,7 @@ def login(self, user: User): """Sign in as a user for all future requests""" self.user = user - access_token_expires = timedelta(hours=24) + access_token_expires = timedelta(hours=24*7) access_token = create_access_token( data={"sub": str(user.user_id)}, expires_delta=access_token_expires ) From 4f3a2924c470667c54dcaf14a5778ea46443a6a6 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 27 Mar 2022 14:04:22 +0200 Subject: [PATCH 101/536] adapt to style guide --- .../views/UsersPage/InviteUser/InviteUser.tsx | 74 +++++++++++++++++++ .../UsersPage/InviteUser/InviteUsers.css | 4 + .../src/views/UsersPage/InviteUser/index.ts | 1 + .../src/views/UsersPage/InviteUser/styles.ts | 60 +++++++++++++++ frontend/src/views/UsersPage/UsersPage.css | 57 -------------- frontend/src/views/UsersPage/UsersPage.tsx | 73 +----------------- 6 files changed, 140 insertions(+), 129 deletions(-) create mode 100644 frontend/src/views/UsersPage/InviteUser/InviteUser.tsx create mode 100644 frontend/src/views/UsersPage/InviteUser/InviteUsers.css create mode 100644 frontend/src/views/UsersPage/InviteUser/index.ts create mode 100644 frontend/src/views/UsersPage/InviteUser/styles.ts delete mode 100644 frontend/src/views/UsersPage/UsersPage.css diff --git a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx new file mode 100644 index 000000000..66d3add2b --- /dev/null +++ b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { getInviteLink } from "../../../utils/api/users"; +import "./InviteUsers.css"; +import { InviteInput, InviteButton, Loader, InviteContainer, Link, Error } from "./styles"; + +export default class InviteUser extends React.Component< + {}, + { + email: string; + valid: boolean; + errorMessage: string | null; + loading: boolean; + link: string | null; + } +> { + constructor(props = {}) { + super(props); + this.state = { email: "", valid: true, errorMessage: null, loading: false, link: null }; + } + + setEmail(email: string) { + this.setState({ email: email, valid: true, link: null, errorMessage: null }); + } + + async sendInvite() { + if (/[^@\s]+@[^@\s]+\.[^@\s]+/.test(this.state.email)) { + this.setState({ loading: true }); + getInviteLink("edition", this.state.email).then(ding => { + this.setState({ link: ding, loading: false }); // TODO: fix email stuff + }); + } else { + this.setState({ valid: false, errorMessage: "Invalid email" }); + } + } + + render() { + let button; + if (this.state.loading) { + button = ; + } else { + button = ( +
+ this.sendInvite()}>Send invite +
+ ); + } + + let error = null; + if (this.state.errorMessage) { + error = {this.state.errorMessage}; + } + + let link = null; + if (this.state.link) { + link = {this.state.link}; + } + + return ( +
+ + this.setEmail(e.target.value)} + /> + {button} + + {error} + {link} +
+ ); + } +} diff --git a/frontend/src/views/UsersPage/InviteUser/InviteUsers.css b/frontend/src/views/UsersPage/InviteUser/InviteUsers.css new file mode 100644 index 000000000..bcbfb0d0f --- /dev/null +++ b/frontend/src/views/UsersPage/InviteUser/InviteUsers.css @@ -0,0 +1,4 @@ + +.email-field-error { + border: 2px solid red !important; +} diff --git a/frontend/src/views/UsersPage/InviteUser/index.ts b/frontend/src/views/UsersPage/InviteUser/index.ts new file mode 100644 index 000000000..f268c1378 --- /dev/null +++ b/frontend/src/views/UsersPage/InviteUser/index.ts @@ -0,0 +1 @@ +export { default as InviteUser } from "./InviteUser"; diff --git a/frontend/src/views/UsersPage/InviteUser/styles.ts b/frontend/src/views/UsersPage/InviteUser/styles.ts new file mode 100644 index 000000000..56d0bb6b3 --- /dev/null +++ b/frontend/src/views/UsersPage/InviteUser/styles.ts @@ -0,0 +1,60 @@ +import styled, { keyframes } from "styled-components"; + +export const InviteContainer = styled.div` + overflow: hidden; +`; + +export const InviteInput = styled.input` + height: 35px; + width: 250px; + font-size: 15px; + margin-top: 10px; + margin-left: 10px; + text-align: center; + border-radius: 5px; + border-width: 0; + float: left; +`; + +export const InviteButton = styled.button` + width: 90px; + height: 35px; + cursor: pointer; + background: var(--osoc_green); + color: white; + border: none; + border-radius: 5px; + margin-left: 7px; + margin-top: 10px; +`; + +const rotate = keyframes` + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +`; + +export const Loader = styled.div` + border: 8px solid var(--osoc_green); + border-top: 8px solid var(--osoc_blue); + border-radius: 50%; + width: 35px; + height: 35px; + animation: ${rotate} 2s linear infinite; + margin-left: 37px; + margin-top: 10px; + float: left; +`; + +export const Link = styled.div` + margin-left: 10px; +`; + +export const Error = styled.div` + margin-left: 10px; + color: var(--osoc_red); +`; diff --git a/frontend/src/views/UsersPage/UsersPage.css b/frontend/src/views/UsersPage/UsersPage.css deleted file mode 100644 index 9634fd692..000000000 --- a/frontend/src/views/UsersPage/UsersPage.css +++ /dev/null @@ -1,57 +0,0 @@ -.invite-user-container { - overflow: hidden; -} - -.invite-user-container input[type="email"] { - height: 30px; - width: 200px; - margin-top: 10px; - margin-left: 10px; - text-align: center; - font-size: 15px; - border-radius: 5px; - border-width: 0; - float: left -} - -.invite-user-container button { - width: 90px; - height: 30px; - cursor: pointer; - background: var(--osoc_green); - color: white; - border: none; - border-radius: 5px; - margin-left: 7px; - margin-top: 10px; -} - -.email-field-error { - border: 2px solid red !important; -} - -.loader { - border: 8px solid var(--osoc_blue); - border-top: 8px solid var(--osoc_green); - border-radius: 50%; - width: 30px; - height: 30px; - animation: spin 2s linear infinite; - margin-left: 37px; - margin-top: 10px; - float: left; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -.link { - margin-left: 10px; -} - -.error { - margin-left: 10px; - color: var(--osoc_red); -} diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index ee42adbb9..ed9a12a2b 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -1,76 +1,5 @@ import React from "react"; -import "./UsersPage.css"; -import { getInviteLink } from "../../utils/api/users"; - -class InviteUser extends React.Component< - {}, - { - email: string; - valid: boolean; - errorMessage: string | null; - loading: boolean; - link: string | null; - } -> { - constructor(props = {}) { - super(props); - this.state = { email: "", valid: true, errorMessage: null, loading: false, link: null }; - } - - setEmail(email: string) { - this.setState({ email: email, valid: true, link: null, errorMessage: null }); - } - - async sendInvite() { - if (/[^@\s]+@[^@\s]+\.[^@\s]+/.test(this.state.email)) { - this.setState({ loading: true }); - getInviteLink("edition", this.state.email).then(ding => { - this.setState({ link: ding, loading: false }); - }); - } else { - this.setState({ valid: false, errorMessage: "Invalid email" }); - } - } - - render() { - let button; - if (this.state.loading) { - button =
; - } else { - button = ( -
- -
- ); - } - let error = null; - if (this.state.errorMessage) { - error =
{this.state.errorMessage}
; - } - let link = null; - if (this.state.link) { - link =
{this.state.link}
; - } - return ( -
-
- this.setEmail(e.target.value)} - /> - {button} -
- {error} - {link} -
- ); - } -} +import { InviteUser } from "./InviteUser"; function UsersPage() { return ; From 5985d0dc36854f197c9096dc9ac6afc451970519 Mon Sep 17 00:00:00 2001 From: beguille Date: Sun, 27 Mar 2022 14:06:07 +0200 Subject: [PATCH 102/536] removed unnecessary fields from classes inheriting from CamelCaseModel --- backend/src/app/logic/register.py | 3 --- backend/src/app/schemas/editions.py | 2 -- backend/src/app/schemas/login.py | 6 ------ backend/src/app/schemas/skills.py | 2 -- backend/src/app/schemas/users.py | 2 -- backend/src/database/crud/register.py | 1 - 6 files changed, 16 deletions(-) diff --git a/backend/src/app/logic/register.py b/backend/src/app/logic/register.py index ad3b5ae6f..d42d7bd80 100644 --- a/backend/src/app/logic/register.py +++ b/backend/src/app/logic/register.py @@ -1,6 +1,3 @@ -import sqlite3 - -import sqlalchemy.exc from sqlalchemy.orm import Session from src.app.schemas.register import NewUser diff --git a/backend/src/app/schemas/editions.py b/backend/src/app/schemas/editions.py index 63d64ba0e..f64cdaa72 100644 --- a/backend/src/app/schemas/editions.py +++ b/backend/src/app/schemas/editions.py @@ -13,7 +13,6 @@ class Edition(CamelCaseModel): class Config: orm_mode = True - allow_population_by_field_name = True class EditionList(CamelCaseModel): @@ -22,4 +21,3 @@ class EditionList(CamelCaseModel): class Config: orm_mode = True - allow_population_by_field_name = True diff --git a/backend/src/app/schemas/login.py b/backend/src/app/schemas/login.py index 79406c8dd..c20799879 100644 --- a/backend/src/app/schemas/login.py +++ b/backend/src/app/schemas/login.py @@ -6,13 +6,7 @@ class Token(CamelCaseModel): access_token: str token_type: str - class Config: - allow_population_by_field_name = True - class User(CamelCaseModel): """The fields used to find a user in the DB""" user_id: int - - class Config: - allow_population_by_field_name = True diff --git a/backend/src/app/schemas/skills.py b/backend/src/app/schemas/skills.py index 9c6e73afc..c000139a2 100644 --- a/backend/src/app/schemas/skills.py +++ b/backend/src/app/schemas/skills.py @@ -15,7 +15,6 @@ class Skill(CamelCaseModel): class Config: orm_mode = True - allow_population_by_field_name = True class SkillList(CamelCaseModel): @@ -24,4 +23,3 @@ class SkillList(CamelCaseModel): class Config: orm_mode = True - allow_population_by_field_name = True diff --git a/backend/src/app/schemas/users.py b/backend/src/app/schemas/users.py index ecd7a1361..612b44ea0 100644 --- a/backend/src/app/schemas/users.py +++ b/backend/src/app/schemas/users.py @@ -10,7 +10,6 @@ class User(CamelCaseModel): class Config: orm_mode = True - allow_population_by_field_name = True class UsersListResponse(CamelCaseModel): @@ -34,7 +33,6 @@ class UserRequest(CamelCaseModel): class Config: orm_mode = True - allow_population_by_field_name = True class UserRequestsResponse(CamelCaseModel): diff --git a/backend/src/database/crud/register.py b/backend/src/database/crud/register.py index e4936e088..b83f7a957 100644 --- a/backend/src/database/crud/register.py +++ b/backend/src/database/crud/register.py @@ -23,7 +23,6 @@ def create_coach_request(db: Session, user: User, edition: Edition) -> CoachRequ def create_auth_email(db: Session, user: User, pw_hash: str, email: str) -> AuthEmail: """Create a authentication for email""" - print("called") auth_email: AuthEmail = AuthEmail(user=user, pw_hash=pw_hash, email=email) db.add(auth_email) db.commit() From 9c242976eebe84d9bff834b8db12c0535bcc26ed Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 27 Mar 2022 14:39:38 +0200 Subject: [PATCH 103/536] convert class component to functional component --- .../views/UsersPage/InviteUser/InviteUser.tsx | 105 +++++++++--------- 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx index 66d3add2b..74dcf122e 100644 --- a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx +++ b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx @@ -1,74 +1,79 @@ -import React from "react"; +import React, { useState } from "react"; import { getInviteLink } from "../../../utils/api/users"; import "./InviteUsers.css"; import { InviteInput, InviteButton, Loader, InviteContainer, Link, Error } from "./styles"; -export default class InviteUser extends React.Component< - {}, - { - email: string; - valid: boolean; - errorMessage: string | null; - loading: boolean; - link: string | null; - } -> { - constructor(props = {}) { - super(props); - this.state = { email: "", valid: true, errorMessage: null, loading: false, link: null }; - } +export default function InviteUser() { + const [email, setEmail] = useState(""); + const [valid, setValid] = useState(true); + const [errorMessage, setErrorMessage] = useState(""); + const [loading, setLoading] = useState(false); + const [link, setLink] = useState(""); - setEmail(email: string) { - this.setState({ email: email, valid: true, link: null, errorMessage: null }); - } + const changeEmail = function (email: string) { + setEmail(email); + setValid(true); + setLink(""); + setErrorMessage(""); + }; - async sendInvite() { - if (/[^@\s]+@[^@\s]+\.[^@\s]+/.test(this.state.email)) { - this.setState({ loading: true }); - getInviteLink("edition", this.state.email).then(ding => { - this.setState({ link: ding, loading: false }); // TODO: fix email stuff + const sendInvite = async () => { + if (/[^@\s]+@[^@\s]+\.[^@\s]+/.test(email)) { + setLoading(true); + getInviteLink("edition", email).then(ding => { + setLink(ding); + setLoading(false); + // TODO: fix email stuff }); } else { - this.setState({ valid: false, errorMessage: "Invalid email" }); + setValid(false); + setErrorMessage("Invalid email"); } - } + }; - render() { + const buttonDiv = () => { let button; - if (this.state.loading) { + if (loading) { button = ; } else { button = (
- this.sendInvite()}>Send invite + sendInvite()}>Send invite
); } + return button; + }; - let error = null; - if (this.state.errorMessage) { - error = {this.state.errorMessage}; + const errorDiv = () => { + let errorDiv = null; + if (errorMessage) { + errorDiv = {errorMessage}; } + return errorDiv; + }; - let link = null; - if (this.state.link) { - link = {this.state.link}; + const linkDiv = () => { + let linkDiv = null; + if (link) { + linkDiv = {link}; } + return linkDiv; + }; - return ( -
- - this.setEmail(e.target.value)} - /> - {button} - - {error} - {link} -
- ); - } + return ( +
+ + changeEmail(e.target.value)} + /> + {buttonDiv()} + + {errorDiv()} + {linkDiv()} +
+ ); } From ae21f6d86845a2b133d95aa58eb32f152009b0fd Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 27 Mar 2022 15:16:14 +0200 Subject: [PATCH 104/536] Refactor InviteUser.tsx --- .../views/UsersPage/InviteUser/InviteUser.tsx | 76 +++++++++---------- .../src/views/UsersPage/InviteUser/styles.ts | 5 +- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx index 74dcf122e..da0fd7593 100644 --- a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx +++ b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx @@ -3,6 +3,36 @@ import { getInviteLink } from "../../../utils/api/users"; import "./InviteUsers.css"; import { InviteInput, InviteButton, Loader, InviteContainer, Link, Error } from "./styles"; +function ButtonDiv(props: { loading: boolean; onClick: () => void }) { + let buttonDiv; + if (props.loading) { + buttonDiv = ; + } else { + buttonDiv = ( +
+ Send invite +
+ ); + } + return buttonDiv; +} + +function ErrorDiv(props: { errorMessage: string }) { + let errorDiv = null; + if (props.errorMessage) { + errorDiv = {props.errorMessage}; + } + return errorDiv; +} + +function LinkDiv(props: { link: string }) { + let linkDiv = null; + if (props.link) { + linkDiv = {props.link}; + } + return linkDiv; +} + export default function InviteUser() { const [email, setEmail] = useState(""); const [valid, setValid] = useState(true); @@ -20,60 +50,28 @@ export default function InviteUser() { const sendInvite = async () => { if (/[^@\s]+@[^@\s]+\.[^@\s]+/.test(email)) { setLoading(true); - getInviteLink("edition", email).then(ding => { - setLink(ding); - setLoading(false); - // TODO: fix email stuff - }); + const ding = await getInviteLink("edition", email); + setLink(ding); + setLoading(false); + // TODO: fix email stuff } else { setValid(false); setErrorMessage("Invalid email"); } }; - const buttonDiv = () => { - let button; - if (loading) { - button = ; - } else { - button = ( -
- sendInvite()}>Send invite -
- ); - } - return button; - }; - - const errorDiv = () => { - let errorDiv = null; - if (errorMessage) { - errorDiv = {errorMessage}; - } - return errorDiv; - }; - - const linkDiv = () => { - let linkDiv = null; - if (link) { - linkDiv = {link}; - } - return linkDiv; - }; - return (
changeEmail(e.target.value)} /> - {buttonDiv()} + - {errorDiv()} - {linkDiv()} + +
); } diff --git a/frontend/src/views/UsersPage/InviteUser/styles.ts b/frontend/src/views/UsersPage/InviteUser/styles.ts index 56d0bb6b3..c7737b5ba 100644 --- a/frontend/src/views/UsersPage/InviteUser/styles.ts +++ b/frontend/src/views/UsersPage/InviteUser/styles.ts @@ -4,7 +4,10 @@ export const InviteContainer = styled.div` overflow: hidden; `; -export const InviteInput = styled.input` +export const InviteInput = styled.input.attrs({ + name: "email", + placeholder: "Invite user by email", +})` height: 35px; width: 250px; font-size: 15px; From c86a2f760182cf73e34ba0f7d520fec452ffa167 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 27 Mar 2022 15:46:37 +0200 Subject: [PATCH 105/536] Making seperate components for login page --- .../SocialButtons/SocialButtons.tsx | 15 +++++++++++ .../LoginComponents/SocialButtons/index.ts | 1 + .../LoginComponents/SocialButtons/styles.ts | 15 +++++++++++ .../WelcomeText/WelcomeText.tsx | 14 ++++++++++ .../LoginComponents/WelcomeText/index.ts | 1 + .../LoginComponents/WelcomeText/styles.ts | 9 +++++++ frontend/src/utils/api/login.ts | 10 ++++--- frontend/src/views/LoginPage/LoginPage.css | 26 +------------------ frontend/src/views/LoginPage/LoginPage.tsx | 26 +++++-------------- frontend/src/views/LoginPage/styles.ts | 7 +++++ 10 files changed, 76 insertions(+), 48 deletions(-) create mode 100644 frontend/src/components/LoginComponents/SocialButtons/SocialButtons.tsx create mode 100644 frontend/src/components/LoginComponents/SocialButtons/index.ts create mode 100644 frontend/src/components/LoginComponents/SocialButtons/styles.ts create mode 100644 frontend/src/components/LoginComponents/WelcomeText/WelcomeText.tsx create mode 100644 frontend/src/components/LoginComponents/WelcomeText/index.ts create mode 100644 frontend/src/components/LoginComponents/WelcomeText/styles.ts create mode 100644 frontend/src/views/LoginPage/styles.ts diff --git a/frontend/src/components/LoginComponents/SocialButtons/SocialButtons.tsx b/frontend/src/components/LoginComponents/SocialButtons/SocialButtons.tsx new file mode 100644 index 000000000..3cca2d539 --- /dev/null +++ b/frontend/src/components/LoginComponents/SocialButtons/SocialButtons.tsx @@ -0,0 +1,15 @@ +import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; +import { SocialsContainer, Socials, GoogleLoginContainer } from "./styles"; + +export default function SocialButtons() { + return ( + + + + + + + + + ); +} diff --git a/frontend/src/components/LoginComponents/SocialButtons/index.ts b/frontend/src/components/LoginComponents/SocialButtons/index.ts new file mode 100644 index 000000000..1aa8d729f --- /dev/null +++ b/frontend/src/components/LoginComponents/SocialButtons/index.ts @@ -0,0 +1 @@ +export { default } from "./SocialButtons"; diff --git a/frontend/src/components/LoginComponents/SocialButtons/styles.ts b/frontend/src/components/LoginComponents/SocialButtons/styles.ts new file mode 100644 index 000000000..8ed2a53d5 --- /dev/null +++ b/frontend/src/components/LoginComponents/SocialButtons/styles.ts @@ -0,0 +1,15 @@ +import styled from "styled-components"; + +export const SocialsContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +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 new file mode 100644 index 000000000..04be7e6c3 --- /dev/null +++ b/frontend/src/components/LoginComponents/WelcomeText/WelcomeText.tsx @@ -0,0 +1,14 @@ +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. +

+
+ ); +} diff --git a/frontend/src/components/LoginComponents/WelcomeText/index.ts b/frontend/src/components/LoginComponents/WelcomeText/index.ts new file mode 100644 index 000000000..e21432cf6 --- /dev/null +++ b/frontend/src/components/LoginComponents/WelcomeText/index.ts @@ -0,0 +1 @@ +export { default } from "./WelcomeText"; diff --git a/frontend/src/components/LoginComponents/WelcomeText/styles.ts b/frontend/src/components/LoginComponents/WelcomeText/styles.ts new file mode 100644 index 000000000..aebc86f01 --- /dev/null +++ b/frontend/src/components/LoginComponents/WelcomeText/styles.ts @@ -0,0 +1,9 @@ +import styled from "styled-components"; + +export const WelcomeTextContainer = styled.div` + max-width: 800px; + text-align: center; + justify-content: center; + margin: auto; + margin-bottom: 50px; +`; diff --git a/frontend/src/utils/api/login.ts b/frontend/src/utils/api/login.ts index 3717e96b5..050c46135 100644 --- a/frontend/src/utils/api/login.ts +++ b/frontend/src/utils/api/login.ts @@ -1,14 +1,18 @@ import axios from "axios"; import { axiosInstance } from "./api"; +interface LoginResponse { + accessToken: string; +} + export async function logIn({ setToken }: any, email: any, password: any) { const payload = new FormData(); payload.append("username", email); payload.append("password", password); try { - await axiosInstance.post("/login/token", payload).then((response: any) => { - setToken(response.data.accessToken); - }); + const response = await axiosInstance.post("/login/token", payload); + const login = response.data as LoginResponse; + await setToken(login.accessToken); return true; } catch (error) { if (axios.isAxiosError(error)) { diff --git a/frontend/src/views/LoginPage/LoginPage.css b/frontend/src/views/LoginPage/LoginPage.css index 0376a8286..158b035d8 100644 --- a/frontend/src/views/LoginPage/LoginPage.css +++ b/frontend/src/views/LoginPage/LoginPage.css @@ -6,28 +6,6 @@ flex-direction: column; } -.welcome-text { - max-width: 800px; - text-align: center; - justify-content: center; - margin-bottom: 50px; -} - -.socials-container { - display: flex; - justify-content: center; - align-items: center; -} - -.socials { - min-width: 230px; - height: fit-content; -} - -.google-login-container { - margin-bottom: 15px; -} - .register-form-content-container { height: fit-content; text-align: center; @@ -88,9 +66,7 @@ input[type="text"] { } .border-right { - margin-right: 20px; - padding-right: 20px; - border-right: 2px solid rgba(182, 182, 182, 0.603); + } .no-account { diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index 2a1ef10ea..f8d679a65 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -1,10 +1,11 @@ import { useState } from "react"; -import OSOCLetters from "../../components/OSOCLetters"; import "./LoginPage.css"; import { logIn } from "../../utils/api/login"; import { useNavigate } from "react-router-dom"; -import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; +import SocialButtons from "../../components/LoginComponents/SocialButtons"; +import WelcomeText from "../../components/LoginComponents/WelcomeText"; +import { VerticalDivider } from "./styles"; function LoginPage({ setToken }: any) { const [email, setEmail] = useState(""); @@ -21,25 +22,10 @@ function LoginPage({ setToken }: any) { 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 quick as possible. -

-
+
- -
-
-
- -
- -
-
-
+ +
Date: Sun, 27 Mar 2022 16:03:09 +0200 Subject: [PATCH 106/536] Add async await --- frontend/frontend_guide.md | 121 +++++++++++++++++++++++++++++++++---- 1 file changed, 109 insertions(+), 12 deletions(-) diff --git a/frontend/frontend_guide.md b/frontend/frontend_guide.md index 3599b4cb9..f126158c1 100644 --- a/frontend/frontend_guide.md +++ b/frontend/frontend_guide.md @@ -85,7 +85,7 @@ interface StudentResponse { async function getStudentName(id: Number): string { const student = (await axiosInstance.get(`/students/${id}`)) as StudentResponse; - return response.name; + return student.name; } ``` @@ -98,7 +98,7 @@ interface StudentResponse { async function getStudentName(id: Number): string { const student = (await axiosInstance.get(`/students/${id}`)) as StudentResponse; - return response.name; + return student.name; } ``` @@ -113,7 +113,66 @@ import { Student } from "../../data/interfaces/students"; async function getStudentName(id: Number): string { const student = (await axiosInstance.get(`/students/${id}`)) as Student; - return response.name; + return student.name; +} +``` + +## Use `async-await` instead of `.then()` + +The old way of handling asynchronous code used `function().then()`. The issue with this is that it becomes quite ugly very quickly, and also leads to the so-called "callback hell". The code executed _after_ the `.then()` **must** be placed inside of it, otherwise there are no guarantees about when or how it will execute. Further, you can't `return` from these callbacks, so things like returning the result of an API call are basically impossible (except for very ugly solutions like creating `Promise`-instances). + +You should only use `.then()` when you really have no other choice. For example: when interacting with something that doesn't _allow_ `async function`s. + +Don't do + +```ts +function callbackHell(): Response { + apiCall().then(response => { + // How do you make "callbackHell()" return "response"? + // "return" doesn't work here, as we are inside of a lambda + otherApiCall().then(otherResponse => { + // How do you make "callbackHell()" return "otherResponse"? + // "return" doesn't work here either, as we are still inside of a lambda + }); + }); +} +``` + +but do + +```ts +// Note the "async" keyword +async function asyncAwaitHeaven(): Promise { + const response = await apiCall(); + const otherResponse = await otherApiCall(); // This line is only executed after the previous one is finished + + // You can now very easily return the response of the API call like you normally would + return response; +} +``` + +Similarly, `.catch()` can be replaced by the `try-catch` pattern you're probably more familiar with. + +Don't do + +```ts +function thenCatch() { + apiCall() + .then(response => console.log(response)) + .catch(exception => console.log(exception)); +} +``` + +but do + +```ts +async function asyncCatch() { + try { + const response = await apiCall(); + console.log(response); + } catch (exception) { + console.log(exception); + } } ``` @@ -162,7 +221,7 @@ This keeps the directories clean, so we don't end up with a thousand `.css` file ## Use `react-bootstrap`-components instead of adding unnecessary Bootstrap `className`s -`react-bootstrap` has a lot of built-in components that do some basic & commonly used functionality for you. It's cleaner to use these components than to make a `
` because it keeps the code more readable. +`react-bootstrap` has a lot of built-in components that do some basic & commonly-used functionality for you. It's cleaner to use these components than to make a `
` because it keeps the code more readable. Don't do @@ -267,12 +326,8 @@ export const PageContent = styled.div` import { PageContent } from "./styles"; export default function Component() { - return ( - {/* Notice how there are no classNames or inline styles, the code is a lot less hectic */} - - { /* more styled-components here */ } - - ); + // Notice how there are no classNames or inline styles, the code is a lot less hectic + return {/* more styled-components here */}; } ``` @@ -362,14 +417,14 @@ export default function SomePage() { ```tsx // Header.tsx export default function Header() { - return
// Either more small components here, or an acceptable amount of code
; + return
{/* Either more small components here, or an acceptable amount of code */}
; } ``` ```tsx // Footer.tsx export default function Footer() { - return
// Either more small components here, or an acceptable amount of code
; + return
{/* Either more small components here, or an acceptable amount of code */}
; } ``` @@ -425,6 +480,7 @@ import { axiosInstance } from "../utils/api"; export default function Component() { return ( + // This makes the .tsx less readable
-
-
-
+ + +
); } diff --git a/frontend/src/views/LoginPage/styles.ts b/frontend/src/views/LoginPage/styles.ts index 825153291..b21ea5e63 100644 --- a/frontend/src/views/LoginPage/styles.ts +++ b/frontend/src/views/LoginPage/styles.ts @@ -1,5 +1,28 @@ import styled from "styled-components"; +export const LoginPageContainer = styled.div` + height: fit-content; + text-align: center; + display: flex; + justify-content: center; + flex-direction: column; + margin: 4%; +`; + +export const LoginContainer = styled.div` + display: flex; + margin-right: auto; + margin-left: auto; +`; + +export const EmailLoginContainer = styled.div` + height: fit-content; + text-align: center; + display: flex; + justify-content: center; + flex-direction: column; +`; + export const VerticalDivider = styled.div` margin-right: 20px; padding-right: 20px; From 7c0ee3af75ba52e4f9bc017a41af4ffe612416e7 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 27 Mar 2022 21:17:44 +0200 Subject: [PATCH 114/536] Filter Pending Requests --- .../PendingRequests/PendingRequests.tsx | 76 +++++++++++++++---- .../views/UsersPage/PendingRequests/styles.ts | 23 +++++- 2 files changed, 81 insertions(+), 18 deletions(-) diff --git a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx index 4950eb569..701942437 100644 --- a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx +++ b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx @@ -1,18 +1,27 @@ import React, { useEffect, useState } from "react"; import Collapsible from "react-collapsible"; import { - RequestHeader, + RequestHeaderTitle, RequestsTable, PendingRequestsContainer, AcceptButton, RejectButton, + SpinnerContainer, + SearchInput, } from "./styles"; import { acceptRequest, getRequests, rejectRequest, Request } from "../../../utils/api/users"; import { Spinner } from "react-bootstrap"; -function RequestsHeader() { - // TODO: Search field when out-folded - return Requests; +function RequestHeader(props: { open: boolean }) { + return Requests {props.open ? "opened" : "closed"}; +} + +function RequestFilter(props: { + search: boolean; + searchTerm: string; + filter: (key: string) => void; +}) { + return props.filter(e.target.value)} />; } function AcceptReject(props: { request_id: number }) { @@ -38,7 +47,11 @@ function RequestItem(props: { request: Request }) { function RequestsList(props: { requests: Request[]; loading: boolean }) { if (props.loading) { - return ; + return ( + + + + ); } else if (props.requests.length === 0) { return
No requests
; } @@ -50,7 +63,6 @@ function RequestsList(props: { requests: Request[]; loading: boolean }) { ))} ); - props.requests.map(request => ); return ( @@ -67,24 +79,56 @@ function RequestsList(props: { requests: Request[]; loading: boolean }) { } export default function PendingRequests(props: { edition: string | undefined }) { + const [allRequests, setAllRequests] = useState([]); const [requests, setRequests] = useState([]); const [gettingRequests, setGettingRequests] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [gotData, setGotData] = useState(false); + const [open, setOpen] = useState(false); useEffect(() => { - getRequests(props.edition) - .then(response => { - setRequests(response.requests); - setGettingRequests(false); - }) - .catch(function (error: any) { - console.log(error); - setGettingRequests(false); - }); + if (!gotData) { + getRequests(props.edition) + .then(response => { + setRequests(response.requests); + setAllRequests(response.requests); + setGettingRequests(false); + setGotData(true); + }) + .catch(function (error: any) { + console.log(error); + setGettingRequests(false); + }); + } }); + const filter = (word: string) => { + setSearchTerm(word); + const newRequests: Request[] = []; + for (const request of allRequests) { + if ( + request.user.name.toUpperCase().includes(word.toUpperCase()) || + request.user.email.toUpperCase().includes(word.toUpperCase()) + ) { + newRequests.push(request); + } + } + setRequests(newRequests); + }; + + // @ts-ignore return ( - }> + } + onOpen={() => setOpen(true)} + onClose={() => setOpen(false)} + > + 0} + searchTerm={searchTerm} + filter={word => filter(word)} + /> diff --git a/frontend/src/views/UsersPage/PendingRequests/styles.ts b/frontend/src/views/UsersPage/PendingRequests/styles.ts index fe3163e33..17e81b40a 100644 --- a/frontend/src/views/UsersPage/PendingRequests/styles.ts +++ b/frontend/src/views/UsersPage/PendingRequests/styles.ts @@ -1,10 +1,22 @@ import styled from "styled-components"; import { Table } from "react-bootstrap"; -export const RequestHeader = styled.div` - background-color: var(--osoc_red); +export const RequestHeaderTitle = styled.div` padding-bottom: 3px; padding-left: 3px; + width: 100px; + font-size: 25px; +`; + +export const SearchInput = styled.input.attrs({ + placeholder: "Search", +})` + margin: 3px; + height: 20px; + width: 150px; + font-size: 15px; + border-radius: 5px; + border-width: 0; `; export const RequestsTable = styled(Table)``; @@ -29,3 +41,10 @@ export const RejectButton = styled.button` padding-left: 3px; padding-right: 3px; `; + +export const SpinnerContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + margin: 20px; +`; From 781fd5c4e0eab315261fdcefb2c73cc6d817f831 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 27 Mar 2022 23:26:13 +0200 Subject: [PATCH 115/536] more changes to login --- .../src/components/LoginComponents/index.ts | 2 ++ frontend/src/views/LoginPage/LoginPage.css | 12 ----------- frontend/src/views/LoginPage/LoginPage.tsx | 21 ++++++++++++------- frontend/src/views/LoginPage/styles.ts | 14 +++++++++++++ 4 files changed, 29 insertions(+), 20 deletions(-) create mode 100644 frontend/src/components/LoginComponents/index.ts diff --git a/frontend/src/components/LoginComponents/index.ts b/frontend/src/components/LoginComponents/index.ts new file mode 100644 index 000000000..4d11681fe --- /dev/null +++ b/frontend/src/components/LoginComponents/index.ts @@ -0,0 +1,2 @@ +export {default as WelcomeText} from "./WelcomeText" +export {default as SocialButtons}from "./SocialButtons" diff --git a/frontend/src/views/LoginPage/LoginPage.css b/frontend/src/views/LoginPage/LoginPage.css index 174bf42bd..34f2dbdef 100644 --- a/frontend/src/views/LoginPage/LoginPage.css +++ b/frontend/src/views/LoginPage/LoginPage.css @@ -22,15 +22,6 @@ border-radius: 10px; } -.login-button { - width: 120px; - height: 35px; - cursor: pointer; - background: var(--osoc_green); - color: white; - border: none; - border-radius: 5px; -} .socials-register { @@ -38,6 +29,3 @@ } -.no-account { - padding-bottom: 15px; -} diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index 3b5662290..f449cd44e 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -3,12 +3,19 @@ import { useNavigate } from "react-router-dom"; import { logIn } from "../../utils/api/login"; -import SocialButtons from "../../components/LoginComponents/SocialButtons"; -import WelcomeText from "../../components/LoginComponents/WelcomeText"; import Email from "../../components/LoginComponents/InputFields/Email"; import Password from "../../components/LoginComponents/InputFields/Password"; -import { LoginPageContainer, LoginContainer, EmailLoginContainer, VerticalDivider } from "./styles"; +import { WelcomeText, SocialButtons } from "../../components/LoginComponents"; + +import { + LoginPageContainer, + LoginContainer, + EmailLoginContainer, + VerticalDivider, + NoAccount, + LoginButton, +} from "./styles"; import "./LoginPage.css"; function LoginPage({ setToken }: any) { @@ -33,13 +40,11 @@ function LoginPage({ setToken }: any) { -
+ Don't have an account? Ask an admin for an invite link -
+
- + Log In
diff --git a/frontend/src/views/LoginPage/styles.ts b/frontend/src/views/LoginPage/styles.ts index b21ea5e63..fe3c9f764 100644 --- a/frontend/src/views/LoginPage/styles.ts +++ b/frontend/src/views/LoginPage/styles.ts @@ -28,3 +28,17 @@ export const VerticalDivider = styled.div` padding-right: 20px; border-right: 2px solid rgba(182, 182, 182, 0.603); `; + +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; +`; From 72e1307bbe6f57b594e184095372c09800b04014 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Mon, 28 Mar 2022 16:05:15 +0200 Subject: [PATCH 116/536] Beter types and import updates --- .../LoginComponents/InputFields/Email/Email.tsx | 8 +++++++- .../LoginComponents/InputFields/Password/Password.tsx | 8 +++++++- frontend/src/components/LoginComponents/index.ts | 6 ++++-- frontend/src/utils/api/login.ts | 2 +- frontend/src/views/LoginPage/LoginPage.tsx | 5 +---- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx b/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx index a392e9d38..2a844b5b1 100644 --- a/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx +++ b/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx @@ -1,6 +1,12 @@ import { Input } from "../styles"; -export default function Email({ email, setEmail }: any) { +export default function Email({ + email, + setEmail, +}: { + email: string; + setEmail: (value: string) => void; +}) { return (
void; +}) { return (
Date: Mon, 28 Mar 2022 16:59:37 +0200 Subject: [PATCH 117/536] started on RegisterPage refactor --- .../ConfirmPassword/ConfirmPassword.tsx | 21 ++++++ .../InputFields/ConfirmPassword/index.ts | 1 + .../InputFields/Email/Email.tsx | 21 ++++++ .../InputFields/Email/index.ts | 1 + .../InputFields/Name/Name.tsx | 21 ++++++ .../InputFields/Name/index.ts | 1 + .../InputFields/Password/Password.tsx | 21 ++++++ .../InputFields/Password/index.ts | 1 + .../RegisterComponents/InputFields/styles.ts | 12 ++++ .../components/RegisterComponents/index.ts | 4 ++ frontend/src/utils/api/index.ts | 2 +- frontend/src/utils/api/register.ts | 29 ++++++++ .../api/{auth.ts => validateRegisterLink.ts} | 2 +- .../src/views/RegisterPage/RegisterPage.tsx | 70 +++++-------------- 14 files changed, 152 insertions(+), 55 deletions(-) create mode 100644 frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx create mode 100644 frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/index.ts create mode 100644 frontend/src/components/RegisterComponents/InputFields/Email/Email.tsx create mode 100644 frontend/src/components/RegisterComponents/InputFields/Email/index.ts create mode 100644 frontend/src/components/RegisterComponents/InputFields/Name/Name.tsx create mode 100644 frontend/src/components/RegisterComponents/InputFields/Name/index.ts create mode 100644 frontend/src/components/RegisterComponents/InputFields/Password/Password.tsx create mode 100644 frontend/src/components/RegisterComponents/InputFields/Password/index.ts create mode 100644 frontend/src/components/RegisterComponents/InputFields/styles.ts create mode 100644 frontend/src/components/RegisterComponents/index.ts create mode 100644 frontend/src/utils/api/register.ts rename frontend/src/utils/api/{auth.ts => validateRegisterLink.ts} (91%) diff --git a/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx b/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx new file mode 100644 index 000000000..1fb5befbd --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx @@ -0,0 +1,21 @@ +import { Input } from "../styles"; + +export default function ConfirmPassword({ + confirmPassword, + setConfirmPassword, +}: { + confirmPassword: string; + setConfirmPassword: (value: string) => void; +}) { + return ( +
+ setConfirmPassword(e.target.value)} + /> +
+ ); +} diff --git a/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/index.ts b/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/index.ts new file mode 100644 index 000000000..4b91ec8f8 --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/index.ts @@ -0,0 +1 @@ +export { default } from "./ConfirmPassword"; diff --git a/frontend/src/components/RegisterComponents/InputFields/Email/Email.tsx b/frontend/src/components/RegisterComponents/InputFields/Email/Email.tsx new file mode 100644 index 000000000..2a844b5b1 --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/Email/Email.tsx @@ -0,0 +1,21 @@ +import { Input } from "../styles"; + +export default function Email({ + email, + setEmail, +}: { + email: string; + setEmail: (value: string) => void; +}) { + return ( +
+ setEmail(e.target.value)} + /> +
+ ); +} diff --git a/frontend/src/components/RegisterComponents/InputFields/Email/index.ts b/frontend/src/components/RegisterComponents/InputFields/Email/index.ts new file mode 100644 index 000000000..f2681e1e2 --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/Email/index.ts @@ -0,0 +1 @@ +export { default } from "./Email"; diff --git a/frontend/src/components/RegisterComponents/InputFields/Name/Name.tsx b/frontend/src/components/RegisterComponents/InputFields/Name/Name.tsx new file mode 100644 index 000000000..35ceedee3 --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/Name/Name.tsx @@ -0,0 +1,21 @@ +import { Input } from "../styles"; + +export default function Name({ + name, + setName, +}: { + name: string; + setName: (value: string) => void; +}) { + return ( +
+ setName(e.target.value)} + /> +
+ ); +} diff --git a/frontend/src/components/RegisterComponents/InputFields/Name/index.ts b/frontend/src/components/RegisterComponents/InputFields/Name/index.ts new file mode 100644 index 000000000..4e90e41d5 --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/Name/index.ts @@ -0,0 +1 @@ +export { default } from "./Name"; diff --git a/frontend/src/components/RegisterComponents/InputFields/Password/Password.tsx b/frontend/src/components/RegisterComponents/InputFields/Password/Password.tsx new file mode 100644 index 000000000..799004c22 --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/Password/Password.tsx @@ -0,0 +1,21 @@ +import { Input } from "../styles"; + +export default function Password({ + password, + setPassword, +}: { + password: string; + setPassword: (value: string) => void; +}) { + return ( +
+ setPassword(e.target.value)} + /> +
+ ); +} diff --git a/frontend/src/components/RegisterComponents/InputFields/Password/index.ts b/frontend/src/components/RegisterComponents/InputFields/Password/index.ts new file mode 100644 index 000000000..b345e9aef --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/Password/index.ts @@ -0,0 +1 @@ +export { default } from "./Password"; diff --git a/frontend/src/components/RegisterComponents/InputFields/styles.ts b/frontend/src/components/RegisterComponents/InputFields/styles.ts new file mode 100644 index 000000000..a0bb3eeba --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/styles.ts @@ -0,0 +1,12 @@ +import styled from "styled-components"; + +export const Input = styled.input` + height: 40px; + width: 400px; + margin-top: 10px; + margin-bottom: 10px; + text-align: center; + font-size: 20px; + border-radius: 10px; + border-width: 0; +`; diff --git a/frontend/src/components/RegisterComponents/index.ts b/frontend/src/components/RegisterComponents/index.ts new file mode 100644 index 000000000..ef1a49d12 --- /dev/null +++ b/frontend/src/components/RegisterComponents/index.ts @@ -0,0 +1,4 @@ +export { default as Email } from "./InputFields/Email"; +export { default as Name } from "./InputFields/Name"; +export { default as Password } from "./InputFields/Password"; +export { default as ConfirmPassword } from "./InputFields/ConfirmPassword"; diff --git a/frontend/src/utils/api/index.ts b/frontend/src/utils/api/index.ts index c4dfe7af3..064c34216 100644 --- a/frontend/src/utils/api/index.ts +++ b/frontend/src/utils/api/index.ts @@ -1 +1 @@ -export { validateRegistrationUrl } from "./auth"; +export { validateRegistrationUrl } from "./validateRegisterLink"; diff --git a/frontend/src/utils/api/register.ts b/frontend/src/utils/api/register.ts new file mode 100644 index 000000000..0314a2a5e --- /dev/null +++ b/frontend/src/utils/api/register.ts @@ -0,0 +1,29 @@ +import axios from "axios"; +import { axiosInstance } from "./api"; + +interface RegisterFields { + email: string; + name: string; + uuid: string; + pw: string; +} + +export async function register( + edition: string, + email: string, + name: string, + uuid: string, + password: string +) { + const payload: RegisterFields = { email: email, name: name, uuid: uuid, pw: password }; + try { + await axiosInstance.post("/editions/" + edition + "/register/email", payload); + return true; + } catch (error) { + if (axios.isAxiosError(error)) { + return false; + } else { + throw error; + } + } +} diff --git a/frontend/src/utils/api/auth.ts b/frontend/src/utils/api/validateRegisterLink.ts similarity index 91% rename from frontend/src/utils/api/auth.ts rename to frontend/src/utils/api/validateRegisterLink.ts index f34d4480c..a5e1a6fba 100644 --- a/frontend/src/utils/api/auth.ts +++ b/frontend/src/utils/api/validateRegisterLink.ts @@ -5,7 +5,7 @@ import { axiosInstance } from "./api"; * Check if a registration url exists by sending a GET to it, * if it returns a 200 then we know the url is valid. */ -export async function validateRegistrationUrl(edition: string, uuid: string): Promise { +export async function validateRegistrationUrl(edition: string, uuid: string | undefined): Promise { try { await axiosInstance.get(`/editions/${edition}/invites/${uuid}`); return true; diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index dc0011ed9..54baedafe 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -1,18 +1,15 @@ import { useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { axiosInstance } from "../../utils/api/api"; -import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; +import { register } from "../../utils/api/register"; +import { validateRegistrationUrl } from "../../utils/api"; -interface RegisterFields { - email: string; - name: string; - uuid: string; - pw: string; -} +import { Email, Name, Password, ConfirmPassword } from "../../components/RegisterComponents"; + +import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; function RegisterPage() { - function register(uuid: string) { + function callRegister(uuid: string) { // Check if passwords are the same if (password !== confirmPassword) { alert("Passwords do not match"); @@ -26,11 +23,7 @@ function RegisterPage() { // TODO this has to change to get the edition the invite belongs to const edition = "1"; - const payload: RegisterFields = { email: email, name: name, uuid: uuid, pw: password }; - - axiosInstance - .post("/editions/" + edition + "/register/email", payload) - .then((response: any) => console.log(response)) + register(edition, email, name, uuid, password) .then(() => navigate("/pending")) .catch(function (error: any) { console.log(error); @@ -49,8 +42,8 @@ function RegisterPage() { const [validUuid, setUuid] = useState(false); - axiosInstance.get("/editions/" + 1 + "/invites/" + uuid).then(response => { - if (response.data.uuid === uuid) { + validateRegistrationUrl("1", uuid).then(response => { + if (response) { setUuid(true); } }); @@ -76,45 +69,16 @@ function RegisterPage() {

or

-
- setEmail(e.target.value)} - /> -
-
- setName(e.target.value)} - /> -
-
- setPassword(e.target.value)} - /> -
-
- setConfirmPassword(e.target.value)} - /> -
+ + + +
-
From 9d9a7f69080b28046b1628aebb18b025a726a9fa Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 28 Mar 2022 17:33:10 +0200 Subject: [PATCH 118/536] Don't allow explicit any type --- frontend/.eslintrc.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 525c87f6b..2b9333731 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -28,6 +28,7 @@ "react/prop-types": "off", "no-use-before-define": "off", "no-unused-vars": "off", + "@typescript-eslint/no-explicit-any": ["error"], "@typescript-eslint/no-unused-vars": ["error"], "@typescript-eslint/no-use-before-define": ["error"] } From 3dffcce2e818776c92b840784e8432f057899229 Mon Sep 17 00:00:00 2001 From: beguille Date: Tue, 29 Mar 2022 12:52:57 +0200 Subject: [PATCH 119/536] fixes #160, fixed as many linting errors as possible --- backend/src/app/exceptions/handlers.py | 8 ++--- backend/src/app/logic/editions.py | 4 +-- backend/src/app/logic/projects.py | 12 ++++---- backend/src/app/logic/security.py | 4 +-- backend/src/app/logic/skills.py | 2 +- backend/src/app/routers/editions/editions.py | 11 ++++--- .../app/routers/editions/projects/projects.py | 10 ++----- .../app/routers/editions/students/students.py | 4 --- .../app/routers/editions/webhooks/webhooks.py | 12 ++++---- backend/src/app/routers/skills/skills.py | 6 ++-- backend/src/app/schemas/editions.py | 2 ++ backend/src/app/schemas/invites.py | 8 +++-- backend/src/app/schemas/projects.py | 8 ++++- backend/src/app/schemas/skills.py | 2 ++ backend/src/app/schemas/users.py | 2 ++ backend/src/app/utils/dependencies.py | 5 ++-- backend/src/database/crud/editions.py | 4 +-- backend/src/database/crud/projects.py | 30 +++++++++---------- .../test_database/test_crud/test_projects.py | 21 +++++++++---- .../test_crud/test_projects_students.py | 2 +- .../test_database/test_crud/test_register.py | 1 - backend/tests/test_database/test_models.py | 2 ++ backend/tests/test_fill_database.py | 2 +- backend/tests/test_logic/test_security.py | 1 + .../test_editions/test_editions.py | 8 ++--- .../test_students/test_students.py | 2 +- .../test_webhooks/test_webhooks.py | 8 +++-- .../test_routers/test_login/test_login.py | 3 ++ .../test_routers/test_skills/test_skills.py | 6 ++-- .../test_routers/test_users/test_users.py | 3 +- backend/tests/test_schemas/test_validators.py | 1 + backend/tests/test_utils/test_mailto.py | 1 + 32 files changed, 110 insertions(+), 85 deletions(-) diff --git a/backend/src/app/exceptions/handlers.py b/backend/src/app/exceptions/handlers.py index dfbda514b..0d9233f65 100644 --- a/backend/src/app/exceptions/handlers.py +++ b/backend/src/app/exceptions/handlers.py @@ -1,14 +1,14 @@ import sqlalchemy.exc -from .editions import DuplicateInsertException from fastapi import FastAPI, Request from fastapi.responses import JSONResponse from pydantic import ValidationError from starlette import status from .authentication import ExpiredCredentialsException, InvalidCredentialsException, MissingPermissionsException +from .editions import DuplicateInsertException from .parsing import MalformedUUIDError -from .webhooks import WebhookProcessException from .register import FailedToAddNewUserException +from .webhooks import WebhookProcessException def install_handlers(app: FastAPI): @@ -74,8 +74,8 @@ def webhook_process_exception(_request: Request, exception: WebhookProcessExcept ) @app.exception_handler(FailedToAddNewUserException) - def failed_to_add_new_user_exception(_request: Request, exception: FailedToAddNewUserException): + def failed_to_add_new_user_exception(_request: Request): return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, content={'message': 'Something went wrong while creating a new user'} - ) \ No newline at end of file + ) diff --git a/backend/src/app/logic/editions.py b/backend/src/app/logic/editions.py index 43969ef13..6b2d06cd9 100644 --- a/backend/src/app/logic/editions.py +++ b/backend/src/app/logic/editions.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import Session -from src.app.schemas.editions import Edition, EditionBase, EditionList -import src.database.crud.editions as crud_editions +from src.app.schemas.editions import EditionBase, EditionList +import src.database.crud.editions as crud_editions from src.database.models import Edition def get_editions(db: Session) -> EditionList: diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index 7ad92f373..e8ce2945f 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session -from src.app.schemas.projects import ProjectList, Project, ConflictStudentList +from src.app.schemas.projects import ProjectList, Project, ConflictStudentList, InputProject from src.database.crud.projects import db_get_all_projects, db_add_project, db_delete_project, \ db_patch_project, db_get_conflict_students from src.database.models import Edition @@ -12,10 +12,9 @@ def logic_get_project_list(db: Session, edition: Edition) -> ProjectList: return ProjectList(projects=db_all_projects) -def logic_create_project(db: Session, edition: Edition, name: str, number_of_students: int, skills: list[int], - partners: list[str], coaches: list[int]) -> Project: +def logic_create_project(db: Session, edition: Edition, input_project: InputProject) -> Project: """Create a new project""" - project = db_add_project(db, edition, name, number_of_students, skills, partners, coaches) + project = db_add_project(db, edition, input_project) return Project(project_id=project.project_id, name=project.name, number_of_students=project.number_of_students, edition_id=project.edition_id, coaches=project.coaches, skills=project.skills, partners=project.partners, project_roles=project.project_roles) @@ -26,10 +25,9 @@ def logic_delete_project(db: Session, project_id: int): db_delete_project(db, project_id) -def logic_patch_project(db: Session, project: Project, name: str, number_of_students: int, skills: list[int], - partners: list[str], coaches: list[int]): +def logic_patch_project(db: Session, project: Project, input_project: InputProject): """Make changes to a project""" - db_patch_project(db, project, name, number_of_students, skills, partners, coaches) + db_patch_project(db, project, input_project) def logic_get_conflicts(db: Session, edition: Edition) -> ConflictStudentList: diff --git a/backend/src/app/logic/security.py b/backend/src/app/logic/security.py index 4564d9ac9..20b07c27f 100644 --- a/backend/src/app/logic/security.py +++ b/backend/src/app/logic/security.py @@ -39,14 +39,14 @@ def get_password_hash(password: str) -> str: return pwd_context.hash(password) -# TODO remove this when the users crud has been implemented +# TO DO remove this when the users crud has been implemented def get_user_by_email(db: Session, email: str) -> models.User: """Find a user by their email address""" auth_email = db.query(models.AuthEmail).where(models.AuthEmail.email == email).one() return db.query(models.User).where(models.User.user_id == auth_email.user_id).one() -# TODO remove this when the users crud has been implemented +# TO DO remove this when the users crud has been implemented def get_user_by_id(db: Session, user_id: int) -> models.User: """Find a user by their id""" return db.query(models.User).where(models.User.user_id == user_id).one() diff --git a/backend/src/app/logic/skills.py b/backend/src/app/logic/skills.py index 562916c3f..00825ba52 100644 --- a/backend/src/app/logic/skills.py +++ b/backend/src/app/logic/skills.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session -from src.app.schemas.skills import Skill, SkillBase, SkillList +from src.app.schemas.skills import SkillBase, SkillList import src.database.crud.skills as crud_skills from src.database.models import Skill diff --git a/backend/src/app/routers/editions/editions.py b/backend/src/app/routers/editions/editions.py index e000cf13d..7d64dc883 100644 --- a/backend/src/app/routers/editions/editions.py +++ b/backend/src/app/routers/editions/editions.py @@ -42,11 +42,12 @@ async def get_editions(db: Session = Depends(get_session)): Returns: EditionList: an object with a list of all the editions. """ - # TODO only return editions the user can see + # TO DO only return editions the user can see return logic_editions.get_editions(db) -@editions_router.get("/{edition_id}", response_model=Edition, tags=[Tags.EDITIONS], dependencies=[Depends(require_coach)]) +@editions_router.get("/{edition_id}", response_model=Edition, tags=[Tags.EDITIONS], + dependencies=[Depends(require_coach)]) async def get_edition_by_id(edition_id: int, db: Session = Depends(get_session)): """Get a specific edition. @@ -60,7 +61,8 @@ async def get_edition_by_id(edition_id: int, db: Session = Depends(get_session)) return logic_editions.get_edition_by_id(db, edition_id) -@editions_router.post("/", status_code=status.HTTP_201_CREATED, response_model=Edition, tags=[Tags.EDITIONS], dependencies=[Depends(require_admin)]) +@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. @@ -73,7 +75,8 @@ async def post_edition(edition: EditionBase, db: Session = Depends(get_session)) return logic_editions.create_edition(db, edition) -@editions_router.delete("/{edition_id}", status_code=status.HTTP_204_NO_CONTENT, tags=[Tags.EDITIONS], dependencies=[Depends(require_admin)]) +@editions_router.delete("/{edition_id}", status_code=status.HTTP_204_NO_CONTENT, tags=[Tags.EDITIONS], + dependencies=[Depends(require_admin)]) async def delete_edition(edition_id: int, db: Session = Depends(get_session)): """Delete an existing edition. diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index d8bdf6406..13b030c85 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -32,9 +32,7 @@ async def create_project(input_project: InputProject, Create a new project """ return logic_create_project(db, edition, - input_project.name, - input_project.number_of_students, - input_project.skills, input_project.partners, input_project.coaches) + input_project) @projects_router.get("/conflicts", response_model=ConflictStudentList) @@ -55,7 +53,7 @@ async def delete_project(project_id: int, db: Session = Depends(get_session)): @projects_router.get("/{project_id}", status_code=status.HTTP_200_OK, response_model=Project) -async def get_project(project: Project = Depends(get_project)): +async def get_project_route(project: Project = Depends(get_project)): """ Get information about a specific project. """ @@ -68,6 +66,4 @@ async def patch_project(input_project: InputProject, project: Project = Depends( """ Update a project, changing some fields. """ - logic_patch_project(db, project, input_project.name, - input_project.number_of_students, - input_project.skills, input_project.partners, input_project.coaches) + logic_patch_project(db, project, input_project) diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index 4e15564e4..9a0bd658d 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -1,9 +1,5 @@ from fastapi import APIRouter -from fastapi import Depends -from src.database.database import get_session -from sqlalchemy.orm import Session - from src.app.routers.tags import Tags from .suggestions import students_suggestions_router diff --git a/backend/src/app/routers/editions/webhooks/webhooks.py b/backend/src/app/routers/editions/webhooks/webhooks.py index 1026780fa..729c8aff9 100644 --- a/backend/src/app/routers/editions/webhooks/webhooks.py +++ b/backend/src/app/routers/editions/webhooks/webhooks.py @@ -1,14 +1,14 @@ from fastapi import APIRouter, Depends from sqlalchemy.orm import Session +from starlette import status -from src.database.database import get_session -from src.database.crud.webhooks import get_webhook, create_webhook +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.database.models import Edition from src.app.utils.dependencies import get_edition, require_admin -from src.app.routers.tags import Tags -from src.app.logic.webhooks import process_webhook -from starlette import status +from src.database.crud.webhooks import get_webhook, create_webhook +from src.database.database import get_session +from src.database.models import Edition webhooks_router = APIRouter(prefix="/webhooks", tags=[Tags.WEBHOOKS]) diff --git a/backend/src/app/routers/skills/skills.py b/backend/src/app/routers/skills/skills.py index 42b60d72d..c43cc40c3 100644 --- a/backend/src/app/routers/skills/skills.py +++ b/backend/src/app/routers/skills/skills.py @@ -24,7 +24,8 @@ async def get_skills(db: Session = Depends(get_session)): return logic_skills.get_skills(db) -@skills_router.post("/",status_code=status.HTTP_201_CREATED, response_model=Skill, tags=[Tags.SKILLS], dependencies=[Depends(require_auth)]) +@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. @@ -38,7 +39,8 @@ async def create_skill(skill: SkillBase, db: Session = Depends(get_session)): 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)]) +@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. diff --git a/backend/src/app/schemas/editions.py b/backend/src/app/schemas/editions.py index f64cdaa72..ed2c9f15a 100644 --- a/backend/src/app/schemas/editions.py +++ b/backend/src/app/schemas/editions.py @@ -12,6 +12,7 @@ class Edition(CamelCaseModel): year: int class Config: + """Set to ORM mode""" orm_mode = True @@ -20,4 +21,5 @@ class EditionList(CamelCaseModel): editions: list[Edition] class Config: + """Set to ORM mode""" orm_mode = True diff --git a/backend/src/app/schemas/invites.py b/backend/src/app/schemas/invites.py index 1ce058d0e..a08b38914 100644 --- a/backend/src/app/schemas/invites.py +++ b/backend/src/app/schemas/invites.py @@ -12,11 +12,12 @@ class EmailAddress(CamelCaseModel): """ email: str + @classmethod @validator("email") - def valid_format(cls, v): + def valid_format(cls, validate): """Check that the email is of a valid format""" - validate_email_format(v) - return v + validate_email_format(validate) + return validate class InviteLink(CamelCaseModel): @@ -29,6 +30,7 @@ class InviteLink(CamelCaseModel): edition_id: int class Config: + """Set to ORM mode""" orm_mode = True diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index 8deac22e4..eb5cd6937 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -10,6 +10,7 @@ class User(CamelCaseModel): name: str class Config: + """Set to ORM mode""" orm_mode = True @@ -20,6 +21,7 @@ class Skill(CamelCaseModel): description: str class Config: + """Set to ORM mode""" orm_mode = True @@ -29,6 +31,7 @@ class Partner(CamelCaseModel): name: str class Config: + """Set to ORM mode""" orm_mode = True @@ -42,6 +45,7 @@ class ProjectRole(CamelCaseModel): drafter_id: int class Config: + """Set to ORM mode""" orm_mode = True @@ -58,6 +62,7 @@ class Project(CamelCaseModel): project_roles: list[ProjectRole] class Config: + """Set to ORM mode""" orm_mode = True @@ -75,6 +80,7 @@ class Student(CamelCaseModel): edition_id: int class Config: + """Set to ORM mode""" orm_mode = True @@ -103,7 +109,7 @@ class InputProject(BaseModel): coaches: list[int] -# TODO: change drafter_id to current user with authentication +# TO DO: change drafter_id to current user with authentication class InputStudentRole(BaseModel): """Used for creating/patching a student role (temporary until authentication is implemented)""" skill_id: int diff --git a/backend/src/app/schemas/skills.py b/backend/src/app/schemas/skills.py index c000139a2..9b56bfdce 100644 --- a/backend/src/app/schemas/skills.py +++ b/backend/src/app/schemas/skills.py @@ -14,6 +14,7 @@ class Skill(CamelCaseModel): description: str | None = None class Config: + """Set to ORM mode""" orm_mode = True @@ -22,4 +23,5 @@ class SkillList(CamelCaseModel): skills: list[Skill] class Config: + """Set to ORM mode""" orm_mode = True diff --git a/backend/src/app/schemas/users.py b/backend/src/app/schemas/users.py index 612b44ea0..bcebacc05 100644 --- a/backend/src/app/schemas/users.py +++ b/backend/src/app/schemas/users.py @@ -9,6 +9,7 @@ class User(CamelCaseModel): admin: bool class Config: + """Set to ORM mode""" orm_mode = True @@ -32,6 +33,7 @@ class UserRequest(CamelCaseModel): user: User class Config: + """Set to ORM mode""" orm_mode = True diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index 9cb4e63f1..73b89c832 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -5,7 +5,8 @@ from sqlalchemy.orm import Session import settings -from src.app.exceptions.authentication import ExpiredCredentialsException, InvalidCredentialsException, MissingPermissionsException +from src.app.exceptions.authentication import ExpiredCredentialsException, InvalidCredentialsException, \ + MissingPermissionsException from src.app.logic.security import ALGORITHM, get_user_by_id from src.database.crud.editions import get_edition_by_id from src.database.crud.projects import db_get_project @@ -14,7 +15,7 @@ from src.database.models import Edition, InviteLink, User, Project -# TODO: Might be nice to use a more descriptive year number here than primary id. +# TO DO: Might be nice to use a more descriptive year number here than primary id. def get_edition(edition_id: int, database: Session = Depends(get_session)) -> Edition: """Get an edition from the database, given the id in the path""" return get_edition_by_id(database, edition_id) diff --git a/backend/src/database/crud/editions.py b/backend/src/database/crud/editions.py index 92f01aecf..26e14d45a 100644 --- a/backend/src/database/crud/editions.py +++ b/backend/src/database/crud/editions.py @@ -4,8 +4,6 @@ from src.database.models import Edition from src.app.schemas.editions import EditionBase -from src.database.models import Edition - def get_edition_by_id(db: Session, edition_id: int) -> Edition: """Get an edition given its primary key @@ -49,7 +47,7 @@ def create_edition(db: Session, edition: EditionBase) -> Edition: db.refresh(new_edition) return new_edition except exc.SQLAlchemyError as exception: - raise DuplicateInsertException(exception) + raise DuplicateInsertException(exception) from exception def delete_edition(db: Session, edition_id: int): diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index fdb410054..551835c92 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -1,7 +1,7 @@ from sqlalchemy.exc import NoResultFound from sqlalchemy.orm import Session -from src.app.schemas.projects import ConflictStudent +from src.app.schemas.projects import ConflictStudent, InputProject from src.database.models import Project, Edition, Student, ProjectRole, Skill, User, Partner @@ -10,24 +10,23 @@ def db_get_all_projects(db: Session, edition: Edition) -> list[Project]: return db.query(Project).where(Project.edition == edition).all() -def db_add_project(db: Session, edition: Edition, name: str, number_of_students: int, skills: list[int], - partners: list[str], coaches: list[int]) -> Project: +def db_add_project(db: Session, edition: Edition, input_project: InputProject) -> 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 skills] - coaches_obj = [db.query(User).where(User.user_id == coach).one() for coach in coaches] + 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 partners: + 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=name, number_of_students=number_of_students, edition_id=edition.edition_id, - skills=skills_obj, coaches=coaches_obj, partners=partners_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) db.add(project) db.commit() @@ -41,7 +40,7 @@ def db_get_project(db: Session, project_id: int) -> Project: def db_delete_project(db: Session, project_id: int): """Delete a specific project from the database""" - # TODO: Maybe make the relationship between project and project_role cascade on delete? + # Maybe make the relationship between project and project_role cascade on delete? # so this code is handled by the database proj_roles = db.query(ProjectRole).where(ProjectRole.project_id == project_id).all() for proj_role in proj_roles: @@ -52,16 +51,15 @@ def db_delete_project(db: Session, project_id: int): db.commit() -def db_patch_project(db: Session, project: Project, name: str, number_of_students: int, skills: list[int], - partners: list[str], coaches: list[int]): +def db_patch_project(db: Session, project: Project, input_project: InputProject): """ Change some fields of a Project in 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 skills] - coaches_obj = [db.query(User).where(User.user_id == coach).one() for coach in coaches] + 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 partners: + for partner in input_project.partners: try: partners_obj.append(db.query(Partner).where(Partner.name == partner).one()) except NoResultFound: @@ -69,8 +67,8 @@ def db_patch_project(db: Session, project: Project, name: str, number_of_student db.add(partner_obj) partners_obj.append(partner_obj) - project.name = name - project.number_of_students = number_of_students + 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 diff --git a/backend/tests/test_database/test_crud/test_projects.py b/backend/tests/test_database/test_crud/test_projects.py index b799598c7..87bebc83f 100644 --- a/backend/tests/test_database/test_crud/test_projects.py +++ b/backend/tests/test_database/test_crud/test_projects.py @@ -2,7 +2,7 @@ from sqlalchemy.orm import Session from sqlalchemy.exc import NoResultFound -from src.app.schemas.projects import ConflictStudent +from src.app.schemas.projects import ConflictStudent, InputProject from src.database.crud.projects import (db_get_all_projects, db_add_project, db_get_project, db_delete_project, db_patch_project, db_get_conflict_students) @@ -73,10 +73,12 @@ def test_get_all_projects(database_with_data: Session, current_edition: Edition) def test_add_project_partner_do_not_exist_yet(database_with_data: Session, current_edition: Edition): """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 = db_add_project( - database_with_data, current_edition, "project1", 2, [1, 3], ["ugent"], [1]) + 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( @@ -95,11 +97,13 @@ def test_add_project_partner_do_not_exist_yet(database_with_data: Session, curre 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 = db_add_project( - database_with_data, current_edition, "project1", 2, [1, 3], ["ugent"], [1]) + 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( @@ -155,16 +159,21 @@ def test_delete_project_with_project_roles(database_with_data: Session, current_ def test_patch_project(database_with_data: Session, current_edition: Edition): """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 = db_add_project( - database_with_data, current_edition, "projec1", 2, [1, 3], ["ugent"], [1]) + 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() db_patch_project(database_with_data, new_project, - "project1", 2, [1, 3], ["ugent"], [1]) + proj_patched) assert new_partner in new_project.partners assert new_project.name == "project1" @@ -177,4 +186,4 @@ def test_get_conflict_students(database_with_data: Session, current_edition: Edi assert conflicts[0].student.student_id == 1 assert len(conflicts[0].projects) == 2 assert conflicts[0].projects[0].project_id == 1 - assert conflicts[0].projects[1].project_id == 2 \ No newline at end of file + assert conflicts[0].projects[1].project_id == 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 b43bfbf41..e959886f2 100644 --- a/backend/tests/test_database/test_crud/test_projects_students.py +++ b/backend/tests/test_database/test_crud/test_projects_students.py @@ -93,4 +93,4 @@ def test_change_project_role_not_assigned_to(database_with_data: Session): project: Project = database_with_data.query( Project).where(Project.project_id == 2).one() with pytest.raises(NoResultFound): - db_change_project_role(database_with_data, project, 2, 2, 1) \ No newline at end of file + db_change_project_role(database_with_data, project, 2, 2, 1) diff --git a/backend/tests/test_database/test_crud/test_register.py b/backend/tests/test_database/test_crud/test_register.py index 2185d9eaa..b7909b6c1 100644 --- a/backend/tests/test_database/test_crud/test_register.py +++ b/backend/tests/test_database/test_crud/test_register.py @@ -18,7 +18,6 @@ def test_react_coach_request(database_session: Session): 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() diff --git a/backend/tests/test_database/test_models.py b/backend/tests/test_database/test_models.py index 230312d82..bf4f12143 100644 --- a/backend/tests/test_database/test_models.py +++ b/backend/tests/test_database/test_models.py @@ -4,6 +4,7 @@ def test_user_coach_request(database_session: Session): + """Test sending a coach request""" edition = models.Edition(year=2022) database_session.add(edition) database_session.commit() @@ -32,6 +33,7 @@ def test_user_coach_request(database_session: Session): def test_project_partners(database_session: Session): + """Test adding a partner to a project""" project = models.Project(name="project") database_session.add(project) database_session.commit() diff --git a/backend/tests/test_fill_database.py b/backend/tests/test_fill_database.py index e702d1de6..d68444d8e 100644 --- a/backend/tests/test_fill_database.py +++ b/backend/tests/test_fill_database.py @@ -3,4 +3,4 @@ def test_fill_database(database_session: Session): """Test that fill_database don't give an error""" - fill_database(database_session) \ No newline at end of file + fill_database(database_session) diff --git a/backend/tests/test_logic/test_security.py b/backend/tests/test_logic/test_security.py index 7bc3b1178..148a9c4eb 100644 --- a/backend/tests/test_logic/test_security.py +++ b/backend/tests/test_logic/test_security.py @@ -2,6 +2,7 @@ def test_hashing(): + """Test the hashing of passwords""" password = "I love inside jokes. I’d love to be a part of one someday" hashed = security.get_password_hash(password) 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 86643afa8..e620aec00 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 @@ -10,7 +10,7 @@ def test_get_editions(database_session: Session, auth_client: AuthClient): Args: database_session (Session): a connection with the database - auth_client (AuthClient): a client used to do rest calls + auth_client (AuthClient): a client used to do rest calls """ edition = Edition(year=2022) database_session.add(edition) @@ -44,7 +44,7 @@ def test_get_edition_by_id_coach(database_session: Session, auth_client: AuthCli Args: database_session (Session): a connection with the database - auth_client (AuthClient): a client used to do rest calls + auth_client (AuthClient): a client used to do rest calls """ edition = Edition(year=2022) database_session.add(edition) @@ -65,7 +65,7 @@ def test_get_edition_by_id_unauthorized(database_session: Session, auth_client: database_session.add(edition) database_session.commit() - assert auth_client.get(f"/editions/1").status_code == status.HTTP_401_UNAUTHORIZED + assert auth_client.get("/editions/1").status_code == status.HTTP_401_UNAUTHORIZED def test_get_edition_by_id_not_coach(database_session: Session, auth_client: AuthClient): @@ -132,7 +132,7 @@ def test_delete_edition_admin(database_session: Session, auth_client: AuthClient Args: database_session (Session): a connection with the database - auth_client (AuthClient): a client used to do rest calls + auth_client (AuthClient): a client used to do rest calls """ auth_client.admin() 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 265222430..1b7963a7d 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 @@ -3,7 +3,7 @@ from sqlalchemy.orm import Session from starlette import status -from src.database.models import Edition, Project, User, Skill, ProjectRole, Student, Partner +from src.database.models import Edition, Project, User, Skill, ProjectRole, Student @pytest.fixture 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 30cc5d3d9..b34a54b73 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 @@ -37,7 +37,7 @@ def test_new_webhook(auth_client: AuthClient, edition: Edition): def test_new_webhook_invalid_edition(auth_client: AuthClient, edition: Edition): auth_client.admin() - response = auth_client.post(f"/editions/0/webhooks/") + response = auth_client.post("/editions/0/webhooks/") assert response.status_code == status.HTTP_404_NOT_FOUND @@ -59,11 +59,12 @@ def test_webhook(test_client: TestClient, webhook: WebhookURL, database_session: assert student.first_name == "Bob" assert student.last_name == "Klonck" assert student.preferred_name == "Jhon" - assert student.wants_to_be_student_coach == False + assert student.wants_to_be_student_coach is False assert student.phone_number == "0477002266" def test_webhook_bad_format(test_client: TestClient, webhook: WebhookURL): + """Test a badly formatted webhook input""" response = test_client.post( f"/editions/{webhook.edition_id}/webhooks/{webhook.uuid}", json=WEBHOOK_EVENT_BAD_FORMAT @@ -72,6 +73,7 @@ def test_webhook_bad_format(test_client: TestClient, webhook: WebhookURL): def test_webhook_duplicate_email(test_client: TestClient, 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", @@ -87,6 +89,7 @@ def test_webhook_duplicate_email(test_client: TestClient, webhook: WebhookURL, m def test_webhook_duplicate_phone(test_client: TestClient, 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", @@ -102,6 +105,7 @@ def test_webhook_duplicate_phone(test_client: TestClient, webhook: WebhookURL, m def test_webhook_missing_question(test_client: TestClient, 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_id}/webhooks/{webhook.uuid}", diff --git a/backend/tests/test_routers/test_login/test_login.py b/backend/tests/test_routers/test_login/test_login.py index b67d14c81..166098cee 100644 --- a/backend/tests/test_routers/test_login/test_login.py +++ b/backend/tests/test_routers/test_login/test_login.py @@ -7,6 +7,7 @@ def test_login_non_existing(test_client: TestClient): + """Test logging in without an existing account""" form = { "username": "this user", "password": "does not exist" @@ -16,6 +17,7 @@ def test_login_non_existing(test_client: TestClient): def test_login_existing(database_session: Session, test_client: TestClient): + """Test logging in with an existing account""" email = "test@ema.il" password = "password" @@ -40,6 +42,7 @@ def test_login_existing(database_session: Session, test_client: TestClient): def test_login_existing_wrong_credentials(database_session: Session, test_client: TestClient): + """Test logging in with existing, but wrong credentials""" email = "test@ema.il" password = "password" diff --git a/backend/tests/test_routers/test_skills/test_skills.py b/backend/tests/test_routers/test_skills/test_skills.py index 326fd08be..a4a93717e 100644 --- a/backend/tests/test_routers/test_skills/test_skills.py +++ b/backend/tests/test_routers/test_skills/test_skills.py @@ -11,7 +11,7 @@ def test_get_skills(database_session: Session, auth_client: AuthClient): Args: database_session (Session): a connection with the database - auth_client (AuthClient): a client used to do rest calls + auth_client (AuthClient): a client used to do rest calls """ auth_client.admin() skill = Skill(name="Backend", description="Must know react") @@ -32,7 +32,7 @@ def test_create_skill(database_session: Session, auth_client: AuthClient): Args: database_session (Session): a connection with the database - auth_client (AuthClient): a client used to do rest calls + auth_client (AuthClient): a client used to do rest calls """ auth_client.admin() @@ -48,7 +48,7 @@ def test_delete_skill(database_session: Session, auth_client: AuthClient): Args: database_session (Session): a connection with the database - auth_client (AuthClient): a client used to do rest calls + auth_client (AuthClient): a client used to do rest calls """ auth_client.admin() diff --git a/backend/tests/test_routers/test_users/test_users.py b/backend/tests/test_routers/test_users/test_users.py index febf9ccf4..1a51fdad4 100644 --- a/backend/tests/test_routers/test_users/test_users.py +++ b/backend/tests/test_routers/test_users/test_users.py @@ -3,7 +3,6 @@ import pytest from sqlalchemy.orm import Session -from starlette.testclient import TestClient from starlette import status from src.database import models @@ -194,7 +193,7 @@ def test_get_all_requests(database_session: Session, auth_client: AuthClient): database_session.commit() - response = auth_client.get(f"/users/requests") + 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 diff --git a/backend/tests/test_schemas/test_validators.py b/backend/tests/test_schemas/test_validators.py index 1612da822..770d621e0 100644 --- a/backend/tests/test_schemas/test_validators.py +++ b/backend/tests/test_schemas/test_validators.py @@ -5,6 +5,7 @@ def test_email_address(): + """Test the validation of email addresses""" with pytest.raises(ValidationException): validate_email_format("test") diff --git a/backend/tests/test_utils/test_mailto.py b/backend/tests/test_utils/test_mailto.py index 5ed6a1822..cd802e171 100644 --- a/backend/tests/test_utils/test_mailto.py +++ b/backend/tests/test_utils/test_mailto.py @@ -2,6 +2,7 @@ def test_mailto(): + """Test generating mailto links""" # Basic assert generate_mailto_string(recipient="me", subject="subject", From 34a5300fbba023e792fb369e7c6c2101fe4de4f5 Mon Sep 17 00:00:00 2001 From: beguille Date: Tue, 29 Mar 2022 13:16:15 +0200 Subject: [PATCH 120/536] linted too much --- backend/src/app/schemas/invites.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/app/schemas/invites.py b/backend/src/app/schemas/invites.py index a08b38914..6224c0f12 100644 --- a/backend/src/app/schemas/invites.py +++ b/backend/src/app/schemas/invites.py @@ -12,7 +12,6 @@ class EmailAddress(CamelCaseModel): """ email: str - @classmethod @validator("email") def valid_format(cls, validate): """Check that the email is of a valid format""" From 27c3bd0193084d26ae96fcfdf818ec419eae9907 Mon Sep 17 00:00:00 2001 From: beguille Date: Tue, 29 Mar 2022 13:24:28 +0200 Subject: [PATCH 121/536] small mistake (linted too much again) --- backend/src/app/exceptions/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/exceptions/handlers.py b/backend/src/app/exceptions/handlers.py index 0d9233f65..3faf655db 100644 --- a/backend/src/app/exceptions/handlers.py +++ b/backend/src/app/exceptions/handlers.py @@ -74,7 +74,7 @@ def webhook_process_exception(_request: Request, exception: WebhookProcessExcept ) @app.exception_handler(FailedToAddNewUserException) - def failed_to_add_new_user_exception(_request: Request): + def failed_to_add_new_user_exception(_request: Request, _exception: FailedToAddNewUserException): return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, content={'message': 'Something went wrong while creating a new user'} From 3ea3960c582d8414cfec676576532164c4f214c1 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq <60451863+stijndcl@users.noreply.github.com> Date: Tue, 29 Mar 2022 14:09:13 +0200 Subject: [PATCH 122/536] Add tsx to lint rule --- frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index b5a0d7f18..95669e887 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -48,7 +48,7 @@ "scripts": { "start": "react-scripts start", "build": "react-scripts build", - "lint": "eslint . --ext js,ts", + "lint": "eslint . --ext js,ts,jsx,tsx", "test": "react-scripts test --watchAll=false" }, "eslintConfig": { From 40451616b4af5e076604710b322689ca35b2cec9 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Tue, 29 Mar 2022 15:35:54 +0200 Subject: [PATCH 123/536] used await functions instead of then --- frontend/src/views/LoginPage/LoginPage.tsx | 11 ++-- .../src/views/RegisterPage/RegisterPage.tsx | 66 ++++++++++--------- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index aaca17aa4..8e9039fcb 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -20,11 +20,14 @@ function LoginPage({ setToken }: any) { const [password, setPassword] = useState(""); const navigate = useNavigate(); - function callLogIn() { - logIn({ setToken }, email, password).then(response => { + async function callLogIn() { + try { + const response = await logIn({ setToken }, email, password); if (response) navigate("/students"); - else alert("Login failed"); - }); + else alert("Something went wrong when login in"); + } catch (error) { + console.log(error); + } } return ( diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index 54baedafe..f5d447836 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -1,15 +1,35 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { register } from "../../utils/api/register"; import { validateRegistrationUrl } from "../../utils/api"; -import { Email, Name, Password, ConfirmPassword } from "../../components/RegisterComponents"; - -import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; +import { + Email, + Name, + Password, + ConfirmPassword, + SocialButtons, +} from "../../components/RegisterComponents"; function RegisterPage() { - function callRegister(uuid: string) { + const [validUuid, setUuid] = useState(false); + const params = useParams(); + const uuid = params.uuid; + + useEffect(() => { + async function validateUuid() { + const response = await validateRegistrationUrl("1", uuid); + if (response) { + setUuid(true); + } + } + if (!validUuid) { + validateUuid(); + } + }, [uuid, validUuid]); + + async function callRegister(uuid: string) { // Check if passwords are the same if (password !== confirmPassword) { alert("Passwords do not match"); @@ -23,11 +43,15 @@ function RegisterPage() { // TODO this has to change to get the edition the invite belongs to const edition = "1"; - register(edition, email, name, uuid, password) - .then(() => navigate("/pending")) - .catch(function (error: any) { - console.log(error); - }); + try { + const response = await register(edition, email, name, uuid, password); + if (response) { + navigate("/pending"); + } + } catch (error) { + console.log(error); + alert("Something went wrong when creating your account"); + } } const [email, setEmail] = useState(""); @@ -37,37 +61,17 @@ function RegisterPage() { const navigate = useNavigate(); - const params = useParams(); - const uuid = params.uuid; - - const [validUuid, setUuid] = useState(false); - - validateRegistrationUrl("1", uuid).then(response => { - if (response) { - setUuid(true); - } - }); - if (validUuid && uuid) { return (

Create an account

-
Sign up with your social media account or email address. Your unique link is not useable again ({uuid})
-
-
- -
- -
-
- +

or

-
From 6b548e6f6cd3d067493f2f87e12d15778bf3d497 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Tue, 29 Mar 2022 15:36:16 +0200 Subject: [PATCH 124/536] moved socialbuttons in different components --- .../SocialButtons/SocialButtons.tsx | 13 +++++++++++++ .../RegisterComponents/SocialButtons/index.ts | 1 + .../RegisterComponents/SocialButtons/styles.ts | 14 ++++++++++++++ .../src/components/RegisterComponents/index.ts | 1 + 4 files changed, 29 insertions(+) create mode 100644 frontend/src/components/RegisterComponents/SocialButtons/SocialButtons.tsx create mode 100644 frontend/src/components/RegisterComponents/SocialButtons/index.ts create mode 100644 frontend/src/components/RegisterComponents/SocialButtons/styles.ts diff --git a/frontend/src/components/RegisterComponents/SocialButtons/SocialButtons.tsx b/frontend/src/components/RegisterComponents/SocialButtons/SocialButtons.tsx new file mode 100644 index 000000000..6d19ea288 --- /dev/null +++ b/frontend/src/components/RegisterComponents/SocialButtons/SocialButtons.tsx @@ -0,0 +1,13 @@ +import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; +import { SocialsContainer, Socials } from "./styles"; + +export default function SocialButtons() { + return ( + + + + + + + ); +} diff --git a/frontend/src/components/RegisterComponents/SocialButtons/index.ts b/frontend/src/components/RegisterComponents/SocialButtons/index.ts new file mode 100644 index 000000000..1aa8d729f --- /dev/null +++ b/frontend/src/components/RegisterComponents/SocialButtons/index.ts @@ -0,0 +1 @@ +export { default } from "./SocialButtons"; diff --git a/frontend/src/components/RegisterComponents/SocialButtons/styles.ts b/frontend/src/components/RegisterComponents/SocialButtons/styles.ts new file mode 100644 index 000000000..4458c3f07 --- /dev/null +++ b/frontend/src/components/RegisterComponents/SocialButtons/styles.ts @@ -0,0 +1,14 @@ +import styled from "styled-components"; + +export const SocialsContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +export const Socials = styled.div` + justify-content: center; + display: flex; + min-width: 230px; + height: fit-content; +`; diff --git a/frontend/src/components/RegisterComponents/index.ts b/frontend/src/components/RegisterComponents/index.ts index ef1a49d12..69e19a874 100644 --- a/frontend/src/components/RegisterComponents/index.ts +++ b/frontend/src/components/RegisterComponents/index.ts @@ -2,3 +2,4 @@ export { default as Email } from "./InputFields/Email"; export { default as Name } from "./InputFields/Name"; export { default as Password } from "./InputFields/Password"; export { default as ConfirmPassword } from "./InputFields/ConfirmPassword"; +export { default as SocialButtons } from "./SocialButtons"; From e952344fb800793f167bc916da6c83adacc1c58d Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Tue, 29 Mar 2022 19:14:08 +0200 Subject: [PATCH 125/536] some more components for Registerpage Redirect when uuid is bad --- .../BadInviteLink/BadInviteLink.tsx | 5 +++ .../RegisterComponents/BadInviteLink/index.ts | 1 + .../BadInviteLink/styles.ts | 9 ++++ .../RegisterComponents/InfoText/InfoText.tsx | 13 ++++++ .../RegisterComponents/InfoText/index.ts | 1 + .../RegisterComponents/InfoText/styles.ts | 10 +++++ .../components/RegisterComponents/index.ts | 2 + frontend/src/views/LoginPage/LoginPage.tsx | 1 - .../src/views/RegisterPage/RegisterPage.tsx | 44 +++++++++---------- .../LoginPage.css => RegisterPage/styles.ts} | 22 ++++------ 10 files changed, 72 insertions(+), 36 deletions(-) create mode 100644 frontend/src/components/RegisterComponents/BadInviteLink/BadInviteLink.tsx create mode 100644 frontend/src/components/RegisterComponents/BadInviteLink/index.ts create mode 100644 frontend/src/components/RegisterComponents/BadInviteLink/styles.ts create mode 100644 frontend/src/components/RegisterComponents/InfoText/InfoText.tsx create mode 100644 frontend/src/components/RegisterComponents/InfoText/index.ts create mode 100644 frontend/src/components/RegisterComponents/InfoText/styles.ts rename frontend/src/views/{LoginPage/LoginPage.css => RegisterPage/styles.ts} (61%) diff --git a/frontend/src/components/RegisterComponents/BadInviteLink/BadInviteLink.tsx b/frontend/src/components/RegisterComponents/BadInviteLink/BadInviteLink.tsx new file mode 100644 index 000000000..db8a330c2 --- /dev/null +++ b/frontend/src/components/RegisterComponents/BadInviteLink/BadInviteLink.tsx @@ -0,0 +1,5 @@ +import { BadInvite } from "./styles"; + +export default function BadInviteLink() { + return Not a valid register url. You will be redirected.; +} diff --git a/frontend/src/components/RegisterComponents/BadInviteLink/index.ts b/frontend/src/components/RegisterComponents/BadInviteLink/index.ts new file mode 100644 index 000000000..c32065aab --- /dev/null +++ b/frontend/src/components/RegisterComponents/BadInviteLink/index.ts @@ -0,0 +1 @@ +export { default } from "./BadInviteLink"; diff --git a/frontend/src/components/RegisterComponents/BadInviteLink/styles.ts b/frontend/src/components/RegisterComponents/BadInviteLink/styles.ts new file mode 100644 index 000000000..9820ef7a2 --- /dev/null +++ b/frontend/src/components/RegisterComponents/BadInviteLink/styles.ts @@ -0,0 +1,9 @@ +import styled from "styled-components"; + +export const BadInvite = styled.div` + margin: auto; + margin-top: 10%; + max-width: 50%; + text-align: center; + font-size: 20px; +`; diff --git a/frontend/src/components/RegisterComponents/InfoText/InfoText.tsx b/frontend/src/components/RegisterComponents/InfoText/InfoText.tsx new file mode 100644 index 000000000..ced927301 --- /dev/null +++ b/frontend/src/components/RegisterComponents/InfoText/InfoText.tsx @@ -0,0 +1,13 @@ +import { TitleText, Info } from "./styles"; + +export default function InfoText() { + return ( +
+ Create an account + + Sign up with your social media account or email address. Your unique link is not + useable again. + +
+ ); +} diff --git a/frontend/src/components/RegisterComponents/InfoText/index.ts b/frontend/src/components/RegisterComponents/InfoText/index.ts new file mode 100644 index 000000000..ef56695bb --- /dev/null +++ b/frontend/src/components/RegisterComponents/InfoText/index.ts @@ -0,0 +1 @@ +export { default } from "./InfoText"; diff --git a/frontend/src/components/RegisterComponents/InfoText/styles.ts b/frontend/src/components/RegisterComponents/InfoText/styles.ts new file mode 100644 index 000000000..319fc8495 --- /dev/null +++ b/frontend/src/components/RegisterComponents/InfoText/styles.ts @@ -0,0 +1,10 @@ +import styled from "styled-components"; + +export const TitleText = styled.h1` + margin-bottom: 10px; +`; + +export const Info = styled.div` + color: grey; + margin-bottom: 10px; +`; diff --git a/frontend/src/components/RegisterComponents/index.ts b/frontend/src/components/RegisterComponents/index.ts index 69e19a874..f60693215 100644 --- a/frontend/src/components/RegisterComponents/index.ts +++ b/frontend/src/components/RegisterComponents/index.ts @@ -3,3 +3,5 @@ export { default as Name } from "./InputFields/Name"; export { default as Password } from "./InputFields/Password"; 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"; diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index 8e9039fcb..ef19fd0bc 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -13,7 +13,6 @@ import { NoAccount, LoginButton, } from "./styles"; -import "./LoginPage.css"; function LoginPage({ setToken }: any) { const [email, setEmail] = useState(""); diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index f5d447836..2fcc1662b 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -10,8 +10,12 @@ import { Password, ConfirmPassword, SocialButtons, + InfoText, + BadInviteLink, } from "../../components/RegisterComponents"; +import { RegisterFormContainer, Or, RegisterButton } from "./styles"; + function RegisterPage() { const [validUuid, setUuid] = useState(false); const params = useParams(); @@ -22,12 +26,16 @@ function RegisterPage() { const response = await validateRegistrationUrl("1", uuid); if (response) { setUuid(true); + } else { + setTimeout(() => { + navigate("/*"); + }, 5000); } } if (!validUuid) { validateUuid(); } - }, [uuid, validUuid]); + }); async function callRegister(uuid: string) { // Check if passwords are the same @@ -64,32 +72,24 @@ function RegisterPage() { if (validUuid && uuid) { return (
-
-

Create an account

-
- Sign up with your social media account or email address. Your unique link is - not useable again ({uuid}) -
+ + -

or

-
- - - - -
+ or + + + +
- + callRegister(uuid)}>Register
-
+
); - } else return
Not a valid register url
; + } else return ; } export default RegisterPage; diff --git a/frontend/src/views/LoginPage/LoginPage.css b/frontend/src/views/RegisterPage/styles.ts similarity index 61% rename from frontend/src/views/LoginPage/LoginPage.css rename to frontend/src/views/RegisterPage/styles.ts index 34f2dbdef..3ae125b1f 100644 --- a/frontend/src/views/LoginPage/LoginPage.css +++ b/frontend/src/views/RegisterPage/styles.ts @@ -1,16 +1,19 @@ +import styled from "styled-components"; - -.register-form-content-container { +export const RegisterFormContainer = styled.div` height: fit-content; text-align: center; display: flex; justify-content: center; flex-direction: column; -} - + margin: 4%; +`; +export const Or = styled.h2` + margin: 10px; +`; -.register-button { +export const RegisterButton = styled.button` height: 40px; width: 400px; margin-top: 10px; @@ -20,12 +23,5 @@ color: white; border: none; border-radius: 10px; -} - - - -.socials-register { - display: flex; -} - +`; From 70831ca6eee7291666535be10b9970ed449a84da Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Wed, 30 Mar 2022 10:51:53 +0200 Subject: [PATCH 126/536] delete student and start of search student --- backend/src/app/logic/students.py | 21 +++++-- backend/src/app/logic/suggestions.py | 13 +++- .../app/routers/editions/students/students.py | 16 ++--- .../students/suggestions/suggestions.py | 10 ++- backend/src/app/schemas/students.py | 38 ++++++++++- backend/src/database/crud/students.py | 24 ++++++- .../test_database/test_crud/test_students.py | 63 +++++++++++++++++-- .../test_students/test_students.py | 29 +++++++++ 8 files changed, 189 insertions(+), 25 deletions(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index af0a19236..7ad484980 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -1,15 +1,26 @@ from sqlalchemy.orm import Session from src.app.schemas.students import NewDecision -from src.database.crud.students import set_definitive_decision_on_student, delete_student -from src.database.models import Student -#from src.app.schemas.suggestion import SuggestionListResponse, SuggestionResponse +from src.database.crud.students import set_definitive_decision_on_student, delete_student, get_students +from src.database.models import Edition, Student +from src.app.schemas.students import ReturnStudentList, ReturnStudent -def definitive_decision_on_student(db: Session, student: Student, decision: NewDecision): +def definitive_decision_on_student(db: Session, student: Student, decision: NewDecision) -> None: """Set a definitive decion on a student""" set_definitive_decision_on_student(db, student, decision.decision) -def remove_student(db: Session, student: Student): +def remove_student(db: Session, student: Student) -> None: """delete a student""" delete_student(db, student) + + +def get_students_search(db: Session, edition: Edition) -> ReturnStudentList: + """return all students""" + students = get_students(db, edition) + return ReturnStudentList(students=students) + + +def get_student_return(student: Student) -> ReturnStudent: + """return a student""" + return ReturnStudent(student=student) diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py index da4b7043e..f35a7d836 100644 --- a/backend/src/app/logic/suggestions.py +++ b/backend/src/app/logic/suggestions.py @@ -6,16 +6,20 @@ from src.app.schemas.suggestion import SuggestionListResponse, SuggestionResponse from src.app.exceptions.authentication import MissingPermissionsException + def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, user: User, student_id: int) -> SuggestionResponse: """"Make a new suggestion""" - suggestion_orm = create_suggestion(db, user.user_id, student_id, new_suggestion.suggestion, new_suggestion.argumentation) + suggestion_orm = create_suggestion( + db, user.user_id, student_id, new_suggestion.suggestion, new_suggestion.argumentation) return SuggestionResponse(suggestion=suggestion_orm) + def all_suggestions_of_student(db: Session, student_id: int) -> SuggestionListResponse: """Get all suggestions of a student""" suggestions_orm = get_suggestions_of_student(db, student_id) return SuggestionListResponse(suggestions=suggestions_orm) + def remove_suggestion(db: Session, suggestion: Suggestion, user: User) -> None: """ Delete a suggestion @@ -28,14 +32,17 @@ def remove_suggestion(db: Session, suggestion: Suggestion, user: User) -> None: else: raise MissingPermissionsException + def change_suggestion(db: Session, new_suggestion: NewSuggestion, suggestion: Suggestion, user: User) -> None: """ Update a suggestion Admins can update all suggestions, coaches only their own suggestions """ if user.admin: - update_suggestion(db,suggestion,new_suggestion.suggestion, new_suggestion.argumentation) + update_suggestion( + db, suggestion, new_suggestion.suggestion, new_suggestion.argumentation) elif suggestion.coach == user: - update_suggestion(db,suggestion,new_suggestion.suggestion, new_suggestion.argumentation) + update_suggestion( + db, suggestion, new_suggestion.suggestion, new_suggestion.argumentation) else: raise MissingPermissionsException diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index c0b2252d1..af769b19d 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -3,8 +3,8 @@ from starlette import status from src.app.routers.tags import Tags -from src.app.utils.dependencies import get_student, get_edition, require_admin -from src.app.logic.students import definitive_decision_on_student, remove_student +from src.app.utils.dependencies import get_student, get_edition, require_admin, require_authorization +from src.app.logic.students import definitive_decision_on_student, remove_student, get_student_return, get_students_search from src.app.schemas.students import NewDecision from src.database.database import get_session from src.database.models import Student, Edition @@ -15,11 +15,12 @@ students_suggestions_router, prefix="/{student_id}") -@students_router.get("/") +@students_router.get("/", dependencies=[Depends(require_authorization)]) async def get_students(db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): """ Get a list of all students. """ + get_students_search(db, edition) @students_router.post("/emails") @@ -34,24 +35,25 @@ async def delete_student(student: Student = Depends(get_student), db: Session = """ Delete all information stored about a specific student. """ - remove_student(db,student) + remove_student(db, student) -@students_router.get("/{student_id}") +@students_router.get("/{student_id}", dependencies=[Depends(require_authorization)]) async def get_student_by_id(edition: Edition = Depends(get_edition), student: Student = Depends(get_student)): """ Get information about a specific student. """ + return get_student_return(student) @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)) -> None: +async def make_decision(decision: NewDecision, student: Student = Depends(get_student), db: Session = Depends(get_session)) -> None: """ 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) + definitive_decision_on_student(db, student, decision) @students_router.get("/{student_id}/emails") diff --git a/backend/src/app/routers/editions/students/suggestions/suggestions.py b/backend/src/app/routers/editions/students/suggestions/suggestions.py index 1283f663a..65163aaff 100644 --- a/backend/src/app/routers/editions/students/suggestions/suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/suggestions.py @@ -10,7 +10,8 @@ from src.app.schemas.suggestion import NewSuggestion, SuggestionListResponse, SuggestionResponse -students_suggestions_router = APIRouter(prefix="/suggestions", tags=[Tags.STUDENTS]) +students_suggestions_router = APIRouter( + prefix="/suggestions", tags=[Tags.STUDENTS]) @students_suggestions_router.post("/", status_code=status.HTTP_201_CREATED, response_model=SuggestionResponse) @@ -20,19 +21,22 @@ async def create_suggestion(new_suggestion: NewSuggestion, student: Student = De """ return 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_authorization), suggestion: Suggestion = Depends(get_suggestion)): """ Delete a suggestion you made about a student. """ - remove_suggestion(db,suggestion,user) + 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_authorization), suggestion: Suggestion = Depends(get_suggestion)): """ Edit a suggestion you made about a student. """ - change_suggestion(db,new_suggestion,suggestion,user) + change_suggestion(db, new_suggestion, suggestion, user) + @students_suggestions_router.get("/", dependencies=[Depends(require_authorization)], status_code=status.HTTP_200_OK, response_model=SuggestionListResponse) async def get_suggestions(student: Student = Depends(get_student), db: Session = Depends(get_session)): diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index 6965f4221..e26803a36 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -1,6 +1,42 @@ from src.app.schemas.webhooks import CamelCaseModel from src.database.enums import DecisionEnum + class NewDecision(CamelCaseModel): """the fields of a decision""" - decision: DecisionEnum \ No newline at end of file + decision: DecisionEnum + + +class Student(CamelCaseModel): + """ + Model to represent a Coach + Sent as a response to API /GET requests + """ + student_id: int + first_name: str + last_name: str + preferred_name: str + email_address: str + phone_number: str + alumni: bool + # cv_url = Column(Text) + decision: DecisionEnum + wants_to_be_student_coach: bool + edition_id: int + + class Config: + orm_mode = True + + +class ReturnStudent(CamelCaseModel): + """ + Model to return a student + """ + student: Student + + +class ReturnStudentList(CamelCaseModel): + """ + Model to return a list of students + """ + students: list[Student] diff --git a/backend/src/database/crud/students.py b/backend/src/database/crud/students.py index cc203e526..9e800e22e 100644 --- a/backend/src/database/crud/students.py +++ b/backend/src/database/crud/students.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session from src.database.enums import DecisionEnum -from src.database.models import Student +from src.database.models import Edition, Skill, Student def get_student_by_id(db: Session, student_id: int) -> Student: @@ -20,3 +20,25 @@ def delete_student(db: Session, student: Student) -> None: """Delete a student from the database""" db.delete(student) db.commit() + + +def get_students(db: Session, edition: Edition ,first_name: str = "", last_name: str = "", alumni: bool = False, student_coach: bool = False, skills: list[Skill] = None) -> list[Student]: + """Get students""" + query = db.query(Student)\ + .where(Student.edition == edition)\ + .where(Student.first_name.contains(first_name))\ + .where(Student.last_name.contains(last_name))\ + + if alumni: + query = query.where(Student.alumni) + + if student_coach: + query = query.where(Student.wants_to_be_student_coach) + + if skills is None: + skills = [] + + for skill in skills: + query = query.where(Student.skills.contains(skill)) + + return query.all() diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index 83bde4d57..d40d95747 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -3,7 +3,7 @@ from sqlalchemy.orm.exc import NoResultFound from src.database.models import Student, User, Edition, Skill from src.database.enums import DecisionEnum -from src.database.crud.students import get_student_by_id, set_definitive_decision_on_student, delete_student +from src.database.crud.students import get_student_by_id, set_definitive_decision_on_student, delete_student, get_students @pytest.fixture @@ -29,8 +29,8 @@ def database_with_data(database_session: Session): 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") + skill4: Skill = Skill(name="skill4", description="important") + skill5: Skill = Skill(name="skill5", description="important") skill6: Skill = Skill(name="skill6", description="something about skill6") database_session.add(skill1) database_session.add(skill2) @@ -45,7 +45,7 @@ def database_with_data(database_session: Session): email_address="josvermeulen@mail.com", phone_number="0487/86.24.45", alumni=True, 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=True, + 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]) database_session.add(student01) @@ -97,4 +97,57 @@ def test_definitive_decision_on_student_no(database_with_data: Session): def test_delete_student(database_with_data: Session): """Tests for deleting a student""" student: Student = get_student_by_id(database_with_data, 1) - delete_student \ No newline at end of file + delete_student(database_with_data, student) + with pytest.raises(NoResultFound): + get_student_by_id(database_with_data, 1) + + +def test_get_all_students(database_with_data: Session): + """test""" + edition: Edition = database_with_data.query(Edition).where(Edition.edition_id == 1).one() + students = get_students(database_with_data, edition) + assert len(students) == 2 + + +def test_search_students_on_first_name(database_with_data: Session): + """test""" + edition: Edition = database_with_data.query(Edition).where(Edition.edition_id == 1).one() + students = get_students(database_with_data, edition, first_name="Jos") + assert len(students) == 1 + + +def test_search_students_on_last_name(database_with_data: Session): + """tests""" + edition: Edition = database_with_data.query(Edition).where(Edition.edition_id == 1).one() + students = get_students(database_with_data, edition, last_name="Vermeulen") + assert len(students) == 1 + + +def test_search_students_alumni(database_with_data: Session): + """tests""" + edition: Edition = database_with_data.query(Edition).where(Edition.edition_id == 1).one() + students = get_students(database_with_data, edition, alumni=True) + assert len(students) == 1 + + +def test_search_students_student_coach(database_with_data: Session): + """tests""" + edition: Edition = database_with_data.query(Edition).where(Edition.edition_id == 1).one() + students = get_students(database_with_data, edition, student_coach=True) + assert len(students) == 1 + + +def test_search_students_one_skill(database_with_data: Session): + """tests""" + 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, skills=[skill]) + assert len(students) == 1 + + +def test_search_students_multiple_skills(database_with_data: Session): + """tests""" + 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, skills=skills) + assert len(students) == 1 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 050d1bb9e..fb2f91c7b 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 @@ -168,12 +168,16 @@ def test_delete_student_coach(database_with_data: Session, test_client: TestClie """tests""" assert test_client.delete("/editions/1/students/2", headers={ "Authorization": auth_coach1}).status_code == status.HTTP_403_FORBIDDEN + students: Student = database_with_data.query(Student).where(Student.student_id == 1).all() + assert len(students) == 1 def test_delete_ghost(database_with_data: Session, test_client: TestClient, auth_admin: str): """tests""" assert test_client.delete("/editions/1/students/100", headers={"Authorization": auth_admin}).status_code == status.HTTP_404_NOT_FOUND + students: Student = database_with_data.query(Student).where(Student.student_id == 1).all() + assert len(students) == 1 def test_delete(database_with_data: Session, test_client: TestClient, auth_admin: str): @@ -182,3 +186,28 @@ def test_delete(database_with_data: Session, test_client: TestClient, auth_admin headers={"Authorization": auth_admin}).status_code == status.HTTP_204_NO_CONTENT students: Student = database_with_data.query(Student).where(Student.student_id == 1).all() assert len(students) == 0 + + +def test_get_student_by_id_no_autorization(database_with_data: Session, test_client: TestClient): + """tests""" + assert test_client.get("/editions/1/students/1", + headers={"Authorization": "auth_admin"}).status_code == status.HTTP_401_UNAUTHORIZED + + +def test_get_student_by_id(database_with_data: Session, test_client: TestClient, auth_admin: str): + """tests""" + assert test_client.get("/editions/1/students/1", + headers={"Authorization": auth_admin}).status_code == status.HTTP_200_OK + + +def test_get_students_no_autorization(database_with_data: Session, test_client: TestClient): + """tests""" + assert test_client.get("/editions/1/students/", + headers={"Authorization": "auth_admin"}).status_code == status.HTTP_401_UNAUTHORIZED + + +def test_get_all_students(database_with_data: Session, test_client: TestClient, auth_admin: str): + """tests""" + response = test_client.get("/editions/1/students/", + headers={"Authorization": auth_admin}) + assert response.status_code == status.HTTP_200_OK From 820467c7673f7ea60699db4d4768f2194564fa8a Mon Sep 17 00:00:00 2001 From: stijndcl Date: Wed, 30 Mar 2022 10:57:40 +0200 Subject: [PATCH 127/536] Add auth to projects --- .../app/routers/editions/projects/projects.py | 18 ++- .../projects/students/projects_students.py | 8 +- .../test_projects/test_projects.py | 123 ++++++++------- .../test_students/test_students.py | 140 ++++++++++-------- 4 files changed, 160 insertions(+), 129 deletions(-) diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index 13b030c85..c97d0cb46 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -8,7 +8,7 @@ from src.app.routers.tags import Tags from src.app.schemas.projects import ProjectList, Project, InputProject, \ ConflictStudentList -from src.app.utils.dependencies import get_edition, get_project +from src.app.utils.dependencies import get_edition, get_project, require_admin, require_coach from src.database.database import get_session from src.database.models import Edition from .students import project_students_router @@ -17,7 +17,7 @@ projects_router.include_router(project_students_router, prefix="/{project_id}") -@projects_router.get("/", response_model=ProjectList) +@projects_router.get("/", response_model=ProjectList, dependencies=[Depends(require_coach)]) async def get_projects(db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): """ Get a list of all projects. @@ -25,7 +25,8 @@ async def get_projects(db: Session = Depends(get_session), edition: Edition = De return logic_get_project_list(db, edition) -@projects_router.post("/", status_code=status.HTTP_201_CREATED, response_model=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: Session = Depends(get_session), edition: Edition = Depends(get_edition)): """ @@ -35,7 +36,7 @@ async def create_project(input_project: InputProject, input_project) -@projects_router.get("/conflicts", response_model=ConflictStudentList) +@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)): """ Get a list of all projects with conflicts, and the users that @@ -44,7 +45,8 @@ async def get_conflicts(db: Session = Depends(get_session), edition: Edition = D return logic_get_conflicts(db, edition) -@projects_router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response) +@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. @@ -52,7 +54,8 @@ async def delete_project(project_id: int, db: Session = Depends(get_session)): return logic_delete_project(db, project_id) -@projects_router.get("/{project_id}", status_code=status.HTTP_200_OK, response_model=Project) +@projects_router.get("/{project_id}", status_code=status.HTTP_200_OK, response_model=Project, + dependencies=[Depends(require_coach)]) async def get_project_route(project: Project = Depends(get_project)): """ Get information about a specific project. @@ -60,7 +63,8 @@ async def get_project_route(project: Project = Depends(get_project)): return project -@projects_router.patch("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response) +@projects_router.patch("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, + dependencies=[Depends(require_admin)]) async def patch_project(input_project: InputProject, project: Project = Depends(get_project), db: Session = Depends(get_session)): """ 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 6ba005853..2a5db43ff 100644 --- a/backend/src/app/routers/editions/projects/students/projects_students.py +++ b/backend/src/app/routers/editions/projects/students/projects_students.py @@ -5,7 +5,7 @@ from src.app.routers.tags import Tags from src.app.schemas.projects import InputStudentRole -from src.app.utils.dependencies import get_project +from src.app.utils.dependencies import get_project, require_coach from src.database.database import get_session from src.database.models import Project from src.app.logic.projects_students import logic_remove_student_project, logic_add_student_project, \ @@ -14,7 +14,7 @@ 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) +@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)): """ @@ -23,7 +23,7 @@ async def remove_student_from_project(student_id: int, db: Session = Depends(get logic_remove_student_project(db, project, student_id) -@project_students_router.patch("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response) +@project_students_router.patch("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, dependencies=[Depends(require_coach)]) async def change_project_role(student_id: int, input_sr: InputStudentRole, db: Session = Depends(get_session), project: Project = Depends(get_project)): """ @@ -32,7 +32,7 @@ async def change_project_role(student_id: int, input_sr: InputStudentRole, db: S logic_change_project_role(db, project, student_id, input_sr.skill_id, input_sr.drafter_id) -@project_students_router.post("/{student_id}", status_code=status.HTTP_201_CREATED, response_class=Response) +@project_students_router.post("/{student_id}", status_code=status.HTTP_201_CREATED, response_class=Response, dependencies=[Depends(require_coach)]) async def add_student_to_project(student_id: int, input_sr: InputStudentRole, db: Session = Depends(get_session), project: Project = Depends(get_project)): """ 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 272f99cc4..f633fbe4a 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,9 +1,9 @@ import pytest -from fastapi.testclient import TestClient from sqlalchemy.orm import Session from starlette import status from src.database.models import Edition, Project, User, Skill, ProjectRole, Student, Partner +from tests.utils.authorization import AuthClient @pytest.fixture @@ -51,9 +51,10 @@ def current_edition(database_with_data: Session) -> Edition: return database_with_data.query(Edition).all()[-1] -def test_get_projects(database_with_data: Session, test_client: TestClient): +def test_get_projects(database_with_data: Session, auth_client: AuthClient): """Tests get all projects""" - response = test_client.get("/editions/1/projects") + auth_client.admin() + response = auth_client.get("/editions/1/projects") json = response.json() assert len(json['projects']) == 3 @@ -62,41 +63,45 @@ def test_get_projects(database_with_data: Session, test_client: TestClient): assert json['projects'][2]['name'] == "project3" -def test_get_project(database_with_data: Session, test_client: TestClient): +def test_get_project(database_with_data: Session, auth_client: AuthClient): """Tests get a specific project""" - response = test_client.get("/editions/1/projects/1") + auth_client.admin() + response = auth_client.get("/editions/1/projects/1") assert response.status_code == status.HTTP_200_OK json = response.json() assert json['name'] == 'project1' -def test_delete_project(database_with_data: Session, test_client: TestClient): +def test_delete_project(database_with_data: Session, auth_client: AuthClient): """Tests delete a project""" - response = test_client.get("/editions/1/projects/1") + auth_client.admin() + response = auth_client.get("/editions/1/projects/1") assert response.status_code == status.HTTP_200_OK - response = test_client.delete("/editions/1/projects/1") + response = auth_client.delete("/editions/1/projects/1") assert response.status_code == status.HTTP_204_NO_CONTENT - response = test_client.get("/editions/1/projects/1") + response = auth_client.get("/editions/1/projects/1") assert response.status_code == status.HTTP_404_NOT_FOUND -def test_delete_ghost_project(database_with_data: Session, test_client: TestClient): - """Tests delete a project that don't exist""" - response = test_client.get("/editions/1/projects/400") +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/1/projects/400") assert response.status_code == status.HTTP_404_NOT_FOUND - response = test_client.delete("/editions/1/projects/400") + response = auth_client.delete("/editions/1/projects/400") assert response.status_code == status.HTTP_404_NOT_FOUND -def test_create_project(database_with_data: Session, test_client: TestClient): - """test create a project""" - response = test_client.get('/editions/1/projects') +def test_create_project(database_with_data: Session, auth_client: AuthClient): + """Tests creating a project""" + auth_client.admin() + response = auth_client.get('/editions/1/projects') json = response.json() assert len(json['projects']) == 3 assert len(database_with_data.query(Partner).all()) == 0 response = \ - test_client.post("/editions/1/projects/", + auth_client.post("/editions/1/projects/", json={"name": "test", "number_of_students": 5, "skills": [1, 1, 1, 1, 1], "partners": ["ugent"], "coaches": [1]}) @@ -106,72 +111,76 @@ def test_create_project(database_with_data: Session, test_client: TestClient): assert response.json()["partners"][0]["name"] == "ugent" assert len(database_with_data.query(Partner).all()) == 1 - response = test_client.get('/editions/1/projects') + response = auth_client.get('/editions/1/projects') json = response.json() assert len(json['projects']) == 4 assert json['projects'][3]['name'] == "test" -def test_create_project_same_partner(database_with_data: Session, test_client: TestClient): - """Tests that creating a project, don't create a partner if the partner allready exist""" +def test_create_project_same_partner(database_with_data: Session, 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 - test_client.post("/editions/1/projects/", + auth_client.post("/editions/1/projects/", json={"name": "test1", "number_of_students": 2, "skills": [1, 2], "partners": ["ugent"], "coaches": [1]}) - test_client.post("/editions/1/projects/", + auth_client.post("/editions/1/projects/", json={"name": "test2", "number_of_students": 2, "skills": [1, 2], "partners": ["ugent"], "coaches": [1]}) assert len(database_with_data.query(Partner).all()) == 1 -def test_create_project_non_existing_skills(database_with_data: Session, test_client: TestClient): - """Tests creating a project with non existing skills""" - response = test_client.get('/editions/1/projects') +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/1/projects') json = response.json() assert len(json['projects']) == 3 assert len(database_with_data.query(Skill).where( Skill.skill_id == 100).all()) == 0 - response = test_client.post("/editions/1/projects/", + response = auth_client.post("/editions/1/projects/", json={"name": "test1", "number_of_students": 1, "skills": [100], "partners": ["ugent"], "coaches": [1]}) assert response.status_code == status.HTTP_404_NOT_FOUND - response = test_client.get('/editions/1/projects') + response = auth_client.get('/editions/1/projects') json = response.json() assert len(json['projects']) == 3 -def test_create_project_non_existing_coach(database_with_data: Session, test_client: TestClient): - """test create a project with a coach that don't exist""" - response = test_client.get('/editions/1/projects') +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/1/projects') json = response.json() assert len(json['projects']) == 3 assert len(database_with_data.query(Student).where( Student.edition_id == 10).all()) == 0 - response = test_client.post("/editions/1/projects/", + response = auth_client.post("/editions/1/projects/", json={"name": "test2", "number_of_students": 1, "skills": [100], "partners": ["ugent"], "coaches": [10]}) assert response.status_code == status.HTTP_404_NOT_FOUND - response = test_client.get('/editions/1/projects') + response = auth_client.get('/editions/1/projects') json = response.json() assert len(json['projects']) == 3 -def test_create_project_no_name(database_with_data: Session, test_client: TestClient): - """Tests when creating a project that has no name""" - response = test_client.get('/editions/1/projects') +def test_create_project_no_name(database_with_data: Session, auth_client: AuthClient): + """Tests creating a project that has no name""" + auth_client.admin() + response = auth_client.get('/editions/1/projects') json = response.json() assert len(json['projects']) == 3 response = \ - test_client.post("/editions/1/projects/", + auth_client.post("/editions/1/projects/", # project has no name json={ "number_of_students": 5, @@ -179,64 +188,66 @@ def test_create_project_no_name(database_with_data: Session, test_client: TestCl assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - response = test_client.get('/editions/1/projects') + response = auth_client.get('/editions/1/projects') json = response.json() assert len(json['projects']) == 3 -def test_patch_project(database_with_data: Session, test_client: TestClient): - """test patch a project""" - - response = test_client.get('/editions/1/projects') +def test_patch_project(database_with_data: Session, auth_client: AuthClient): + """Tests patching a project""" + auth_client.admin() + response = auth_client.get('/editions/1/projects') json = response.json() assert len(json['projects']) == 3 - response = test_client.patch("/editions/1/projects/1", + response = auth_client.patch("/editions/1/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 = test_client.get('/editions/1/projects') + response = auth_client.get('/editions/1/projects') json = response.json() assert len(json['projects']) == 3 assert json['projects'][0]['name'] == 'patched' -def test_patch_project_non_existing_skills(database_with_data: Session, test_client: TestClient): - """Tests patch a project with non existing skills""" +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 - response = test_client.patch("/editions/1/projects/1", + response = auth_client.patch("/editions/1/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 = test_client.get("/editions/1/projects/1") + response = auth_client.get("/editions/1/projects/1") json = response.json() assert 100 not in json["skills"] -def test_patch_project_non_existing_coach(database_with_data: Session, test_client: TestClient): - """test patch a project with a coach that don't exist""" - +def test_patch_project_non_existing_coach(database_with_data: Session, 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 - response = test_client.patch("/editions/1/projects/1", + response = auth_client.patch("/editions/1/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 = test_client.get("/editions/1/projects/1") + response = auth_client.get("/editions/1/projects/1") json = response.json() assert 10 not in json["coaches"] -def test_patch_wrong_project(database_session: Session, test_client: TestClient): - """tests patch with wrong project info""" +def test_patch_wrong_project(database_session: Session, auth_client: AuthClient): + """Tests patching with wrong project info""" + auth_client.admin() database_session.add(Edition(year=2022)) project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) @@ -244,12 +255,12 @@ def test_patch_wrong_project(database_session: Session, test_client: TestClient) database_session.commit() response = \ - test_client.patch("/editions/1/projects/1", + auth_client.patch("/editions/1/projects/1", json={"name": "patched", "skills": [], "partners": [], "coaches": []}) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - response2 = test_client.get('/editions/1/projects') + response2 = auth_client.get('/editions/1/projects') json = response2.json() assert len(json['projects']) == 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 1b7963a7d..055273e5c 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 @@ -4,6 +4,7 @@ from starlette import status from src.database.models import Edition, Project, User, Skill, ProjectRole, Student +from tests.utils.authorization import AuthClient @pytest.fixture @@ -57,227 +58,242 @@ def current_edition(database_with_data: Session) -> Edition: return database_with_data.query(Edition).all()[-1] -def test_add_student_project(database_with_data: Session, test_client: TestClient): - """tests add a student to a project""" - resp = test_client.post( +def test_add_student_project(database_with_data: Session, auth_client: AuthClient): + """Tests adding a student to a project""" + auth_client.admin() + resp = auth_client.post( "/editions/1/projects/1/students/3", json={"skill_id": 1, "drafter_id": 1}) assert resp.status_code == status.HTTP_201_CREATED - response2 = test_client.get('/editions/1/projects') + response2 = auth_client.get('/editions/1/projects') json = response2.json() assert len(json['projects'][0]['projectRoles']) == 3 assert json['projects'][0]['projectRoles'][2]['skillId'] == 1 -def test_add_ghost_student_project(database_with_data: Session, test_client: TestClient): - """tests add a non existing student to a project""" +def test_add_ghost_student_project(database_with_data: Session, auth_client: AuthClient): + """Tests adding a non-existing student to a project""" + auth_client.admin() student10: list[Student] = database_with_data.query( Student).where(Student.student_id == 10).all() assert len(student10) == 0 - response = test_client.get('/editions/1/projects/1') + response = auth_client.get('/editions/1/projects/1') json = response.json() assert len(json['projectRoles']) == 2 - resp = test_client.post( + resp = auth_client.post( "/editions/1/projects/1/students/10", json={"skill_id": 1, "drafter_id": 1}) assert resp.status_code == status.HTTP_404_NOT_FOUND - response = test_client.get('/editions/1/projects/1') + response = auth_client.get('/editions/1/projects/1') json = response.json() assert len(json['projectRoles']) == 2 -def test_add_student_project_non_existing_skill(database_with_data: Session, test_client: TestClient): - """tests add a non existing student to a project""" +def test_add_student_project_non_existing_skill(database_with_data: Session, auth_client: AuthClient): + """Tests adding a non-existing student to a project""" + auth_client.admin() skill10: list[Skill] = database_with_data.query( Skill).where(Skill.skill_id == 10).all() assert len(skill10) == 0 - response = test_client.get('/editions/1/projects/1') + response = auth_client.get('/editions/1/projects/1') json = response.json() assert len(json['projectRoles']) == 2 - resp = test_client.post( + resp = auth_client.post( "/editions/1/projects/1/students/3", json={"skill_id": 10, "drafter_id": 1}) assert resp.status_code == status.HTTP_404_NOT_FOUND - response = test_client.get('/editions/1/projects/1') + response = auth_client.get('/editions/1/projects/1') json = response.json() assert len(json['projectRoles']) == 2 -def test_add_student_project_ghost_drafter(database_with_data: Session, test_client: TestClient): - """test add a student to a project with a drafter that don't exist""" +def test_add_student_project_ghost_drafter(database_with_data: Session, auth_client: AuthClient): + """Tests adding a student to a project with a drafter that doesn't exist""" + auth_client.admin() user10: list[User] = database_with_data.query( User).where(User.user_id == 10).all() assert len(user10) == 0 - response = test_client.get('/editions/1/projects/1') + response = auth_client.get('/editions/1/projects/1') json = response.json() assert len(json['projectRoles']) == 2 - resp = test_client.post( + resp = auth_client.post( "/editions/1/projects/1/students/3", json={"skill_id": 1, "drafter_id": 10}) assert resp.status_code == status.HTTP_404_NOT_FOUND - response = test_client.get('/editions/1/projects/1') + response = auth_client.get('/editions/1/projects/1') json = response.json() assert len(json['projectRoles']) == 2 -def test_add_student_to_ghost_project(database_with_data: Session, test_client: TestClient): - """test add a student to a project that don't exist""" +def test_add_student_to_ghost_project(database_with_data: Session, auth_client: AuthClient): + """Tests adding a student to a project that doesn't exist""" + auth_client.admin() project10: list[Project] = database_with_data.query( Project).where(Project.project_id == 10).all() assert len(project10) == 0 - resp = test_client.post( + resp = auth_client.post( "/editions/1/projects/10/students/1", json={"skill_id": 1, "drafter_id": 1}) assert resp.status_code == status.HTTP_404_NOT_FOUND -def test_add_incomplete_data_student_project(database_session: Session, test_client: TestClient): - """test add a student with incomplete data""" +def test_add_incomplete_data_student_project(database_session: Session, auth_client: AuthClient): + """Tests adding a student with incomplete data""" + auth_client.admin() database_session.add(Edition(year=2022)) project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) database_session.add(project) database_session.commit() - resp = test_client.post( + resp = auth_client.post( "/editions/1/projects/1/students/1", json={"drafter_id": 1}) assert resp.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - response2 = test_client.get('/editions/1/projects') + response2 = auth_client.get('/editions/1/projects') json = response2.json() assert len(json['projects'][0]['projectRoles']) == 0 -def test_change_student_project(database_with_data: Session, test_client: TestClient): - """test change a student project""" - resp1 = test_client.patch( +def test_change_student_project(database_with_data: Session, auth_client: AuthClient): + """Tests changing a student's project""" + auth_client.admin() + resp1 = auth_client.patch( "/editions/1/projects/1/students/1", json={"skill_id": 2, "drafter_id": 1}) assert resp1.status_code == status.HTTP_204_NO_CONTENT - response2 = test_client.get('/editions/1/projects') + response2 = auth_client.get('/editions/1/projects') json = response2.json() assert len(json['projects'][0]['projectRoles']) == 2 assert json['projects'][0]['projectRoles'][0]['skillId'] == 2 -def test_change_incomplete_data_student_project(database_with_data: Session, test_client: TestClient): - """test change student project with incomplete data""" - resp1 = test_client.patch( +def test_change_incomplete_data_student_project(database_with_data: Session, auth_client: AuthClient): + """Tests changing a student's project with incomplete data""" + auth_client.admin() + resp1 = auth_client.patch( "/editions/1/projects/1/students/1", json={"skill_id": 2}) assert resp1.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - response2 = test_client.get('/editions/1/projects') + response2 = auth_client.get('/editions/1/projects') json = response2.json() assert len(json['projects'][0]['projectRoles']) == 2 assert json['projects'][0]['projectRoles'][0]['skillId'] == 1 -def test_change_ghost_student_project(database_with_data: Session, test_client: TestClient): - """tests change a non existing student of a project""" +def test_change_ghost_student_project(database_with_data: Session, auth_client: AuthClient): + """Tests changing a non-existing student of a project""" + auth_client.admin() student10: list[Student] = database_with_data.query( Student).where(Student.student_id == 10).all() assert len(student10) == 0 - response = test_client.get('/editions/1/projects/1') + response = auth_client.get('/editions/1/projects/1') json = response.json() assert len(json['projectRoles']) == 2 - resp = test_client.patch( + resp = auth_client.patch( "/editions/1/projects/1/students/10", json={"skill_id": 1, "drafter_id": 1}) assert resp.status_code == status.HTTP_404_NOT_FOUND - response = test_client.get('/editions/1/projects/1') + response = auth_client.get('/editions/1/projects/1') json = response.json() assert len(json['projectRoles']) == 2 -def test_change_student_project_non_existing_skill(database_with_data: Session, test_client: TestClient): - """test change a skill of a projectRole to a non-existing one""" +def test_change_student_project_non_existing_skill(database_with_data: Session, auth_client: AuthClient): + """Test changing the skill of a ProjectRole to a non-existing one""" + auth_client.admin() skill10: list[Skill] = database_with_data.query( Skill).where(Skill.skill_id == 10).all() assert len(skill10) == 0 - response = test_client.get('/editions/1/projects/1') + response = auth_client.get('/editions/1/projects/1') json = response.json() assert len(json['projectRoles']) == 2 - resp = test_client.patch( + resp = auth_client.patch( "/editions/1/projects/1/students/3", json={"skill_id": 10, "drafter_id": 1}) assert resp.status_code == status.HTTP_404_NOT_FOUND - response = test_client.get('/editions/1/projects/1') + response = auth_client.get('/editions/1/projects/1') json = response.json() assert len(json['projectRoles']) == 2 -def test_change_student_project_ghost_drafter(database_with_data: Session, test_client: TestClient): - """test change a drafter of a projectRole to a non-existing one""" +def test_change_student_project_ghost_drafter(database_with_data: Session, auth_client: AuthClient): + """Tests changing a drafter of a ProjectRole to a non-existing one""" + auth_client.admin() user10: list[User] = database_with_data.query( User).where(User.user_id == 10).all() assert len(user10) == 0 - response = test_client.get('/editions/1/projects/1') + response = auth_client.get('/editions/1/projects/1') json = response.json() assert len(json['projectRoles']) == 2 - resp = test_client.patch( + resp = auth_client.patch( "/editions/1/projects/1/students/3", json={"skill_id": 1, "drafter_id": 10}) assert resp.status_code == status.HTTP_404_NOT_FOUND - response = test_client.get('/editions/1/projects/1') + response = auth_client.get('/editions/1/projects/1') json = response.json() assert len(json['projectRoles']) == 2 -def test_change_student_to_ghost_project(database_with_data: Session, test_client: TestClient): - """test change a student of a project that don't exist""" +def test_change_student_to_ghost_project(database_with_data: Session, auth_client: AuthClient): + """Tests changing a student of a project that doesn't exist""" + auth_client.admin() project10: list[Project] = database_with_data.query( Project).where(Project.project_id == 10).all() assert len(project10) == 0 - resp = test_client.patch( + resp = auth_client.patch( "/editions/1/projects/10/students/1", json={"skill_id": 1, "drafter_id": 1}) assert resp.status_code == status.HTTP_404_NOT_FOUND -def test_delete_student_project(database_with_data: Session, test_client: TestClient): - """test delete a student from a project""" - resp = test_client.delete("/editions/1/projects/1/students/1") +def test_delete_student_project(database_with_data: Session, auth_client: AuthClient): + """Tests deleting a student from a project""" + auth_client.admin() + resp = auth_client.delete("/editions/1/projects/1/students/1") assert resp.status_code == status.HTTP_204_NO_CONTENT - response2 = test_client.get('/editions/1/projects') + response2 = auth_client.get('/editions/1/projects') json = response2.json() assert len(json['projects'][0]['projectRoles']) == 1 -def test_delete_student_project_empty(database_session: Session, test_client: TestClient): - """delete a student from a project that isn't asigned""" +def test_delete_student_project_empty(database_session: Session, auth_client: AuthClient): + """Tests deleting a student from a project that isn't assigned""" + auth_client.admin() database_session.add(Edition(year=2022)) project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) database_session.add(project) database_session.commit() - resp = test_client.delete("/editions/1/projects/1/students/1") + resp = auth_client.delete("/editions/1/projects/1/students/1") assert resp.status_code == status.HTTP_404_NOT_FOUND -def test_get_conflicts(database_with_data: Session, test_client: TestClient): - """test get the conflicts""" - response = test_client.get("/editions/1/projects/conflicts") +def test_get_conflicts(database_with_data: Session, auth_client: AuthClient): + """Test getting the conflicts""" + auth_client.admin() + response = auth_client.get("/editions/1/projects/conflicts") json = response.json() assert len(json['conflictStudents']) == 1 assert json['conflictStudents'][0]['student']['studentId'] == 1 From 05119a92bcf6d8f0a7e30a6c7710a5981dcf8802 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Wed, 30 Mar 2022 11:04:02 +0200 Subject: [PATCH 128/536] fixes more linting errors --- .../students/suggestions/suggestions.py | 22 +++++++++++++------ .../test_suggestions/test_suggestions.py | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/backend/src/app/routers/editions/students/suggestions/suggestions.py b/backend/src/app/routers/editions/students/suggestions/suggestions.py index 1283f663a..e08f39ace 100644 --- a/backend/src/app/routers/editions/students/suggestions/suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/suggestions.py @@ -6,33 +6,41 @@ from src.app.utils.dependencies import require_authorization, get_student, get_suggestion 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, remove_suggestion, change_suggestion +from src.app.logic.suggestions import (make_new_suggestion, all_suggestions_of_student, + remove_suggestion, change_suggestion) from src.app.schemas.suggestion import NewSuggestion, SuggestionListResponse, SuggestionResponse -students_suggestions_router = APIRouter(prefix="/suggestions", tags=[Tags.STUDENTS]) +students_suggestions_router = APIRouter( + prefix="/suggestions", tags=[Tags.STUDENTS]) @students_suggestions_router.post("/", status_code=status.HTTP_201_CREATED, response_model=SuggestionResponse) -async def create_suggestion(new_suggestion: NewSuggestion, student: Student = Depends(get_student), db: Session = Depends(get_session), user: User = Depends(require_authorization)): +async def create_suggestion(new_suggestion: NewSuggestion, student: Student = Depends(get_student), + db: Session = Depends(get_session), user: User = Depends(require_authorization)): """ Make a suggestion about a student. """ return 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_authorization), suggestion: Suggestion = Depends(get_suggestion)): +async def delete_suggestion(db: Session = Depends(get_session), user: User = Depends(require_authorization), + suggestion: Suggestion = Depends(get_suggestion)): """ Delete a suggestion you made about a student. """ - remove_suggestion(db,suggestion,user) + 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_authorization), suggestion: Suggestion = Depends(get_suggestion)): +async def edit_suggestion(new_suggestion: NewSuggestion, db: Session = Depends(get_session), + user: User = Depends(require_authorization), suggestion: Suggestion = Depends(get_suggestion)): """ Edit a suggestion you made about a student. """ - change_suggestion(db,new_suggestion,suggestion,user) + change_suggestion(db, new_suggestion, suggestion, user) + @students_suggestions_router.get("/", dependencies=[Depends(require_authorization)], status_code=status.HTTP_200_OK, response_model=SuggestionListResponse) async def get_suggestions(student: Student = Depends(get_student), db: Session = Depends(get_session)): 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 1f012a578..2eba0cf3a 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,3 +1,4 @@ +from re import A import pytest from sqlalchemy.orm import Session from starlette import status @@ -120,7 +121,6 @@ def test_new_suggestion(database_with_data: Session, test_client: TestClient, au suggestions: list[Suggestion] = database_with_data.query( Suggestion).where(Suggestion.student_id == 2).all() assert len(suggestions) == 1 - print(resp.json()) assert resp.json()[ "suggestion"]["coach"]["email"] == suggestions[0].coach.email assert DecisionEnum(resp.json()["suggestion"] From beac71e6c58fe434a85f78751fd46517c647b515 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Wed, 30 Mar 2022 11:16:39 +0200 Subject: [PATCH 129/536] linting errors + failing tests fixed --- .../editions/students/suggestions/suggestions.py | 10 +++++----- .../test_students/test_suggestions/test_suggestions.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/src/app/routers/editions/students/suggestions/suggestions.py b/backend/src/app/routers/editions/students/suggestions/suggestions.py index e08f39ace..022216009 100644 --- a/backend/src/app/routers/editions/students/suggestions/suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/suggestions.py @@ -3,7 +3,7 @@ from starlette import status from src.app.routers.tags import Tags -from src.app.utils.dependencies import require_authorization, get_student, get_suggestion +from src.app.utils.dependencies import require_auth, get_student, get_suggestion 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, @@ -17,7 +17,7 @@ @students_suggestions_router.post("/", status_code=status.HTTP_201_CREATED, response_model=SuggestionResponse) async def create_suggestion(new_suggestion: NewSuggestion, student: Student = Depends(get_student), - db: Session = Depends(get_session), user: User = Depends(require_authorization)): + db: Session = Depends(get_session), user: User = Depends(require_auth)): """ Make a suggestion about a student. """ @@ -25,7 +25,7 @@ async def create_suggestion(new_suggestion: NewSuggestion, student: Student = De @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_authorization), +async def delete_suggestion(db: Session = Depends(get_session), user: User = Depends(require_auth), suggestion: Suggestion = Depends(get_suggestion)): """ Delete a suggestion you made about a student. @@ -35,14 +35,14 @@ async def delete_suggestion(db: Session = Depends(get_session), user: User = Dep @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_authorization), suggestion: Suggestion = Depends(get_suggestion)): + user: User = Depends(require_auth), suggestion: Suggestion = Depends(get_suggestion)): """ Edit a suggestion you made about a student. """ change_suggestion(db, new_suggestion, suggestion, user) -@students_suggestions_router.get("/", dependencies=[Depends(require_authorization)], status_code=status.HTTP_200_OK, response_model=SuggestionListResponse) +@students_suggestions_router.get("/", dependencies=[Depends(require_auth)], status_code=status.HTTP_200_OK, response_model=SuggestionListResponse) async def get_suggestions(student: Student = Depends(get_student), db: Session = Depends(get_session)): """ Get all suggestions of a student. 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 2eba0cf3a..8c7d449c4 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 @@ -18,10 +18,10 @@ def database_with_data(database_session: Session) -> Session: database_session.commit() # Users - admin: User = User(name="admin", email="admin@ngmail.com", admin=True) - coach1: User = User(name="coach1", email="coach1@noutlook.be") - coach2: User = User(name="coach2", email="coach2@noutlook.be") - request: User = User(name="request", email="request@ngmail.com") + admin: User = User(name="admin", email="admin@ngmail.com", admin=True, editions=[edition]) + coach1: User = User(name="coach1", email="coach1@noutlook.be", editions=[edition]) + coach2: User = User(name="coach2", email="coach2@noutlook.be", editions=[edition]) + request: User = User(name="request", email="request@ngmail.com", editions=[edition]) database_session.add(admin) database_session.add(coach1) database_session.add(coach2) From 685b347a2bb6198f3147d2a7fa75802f2eb6f70f Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 30 Mar 2022 12:15:00 +0200 Subject: [PATCH 130/536] removed some css in favour of styled components --- frontend/src/views/ErrorPage/ErrorPage.tsx | 6 +++--- frontend/src/views/ErrorPage/{ErrorPage.css => styles.ts} | 6 ++++-- frontend/src/views/RegisterPage/RegisterPage.css | 0 3 files changed, 7 insertions(+), 5 deletions(-) rename frontend/src/views/ErrorPage/{ErrorPage.css => styles.ts} (50%) delete mode 100644 frontend/src/views/RegisterPage/RegisterPage.css diff --git a/frontend/src/views/ErrorPage/ErrorPage.tsx b/frontend/src/views/ErrorPage/ErrorPage.tsx index e82b5ad8a..5b2679219 100644 --- a/frontend/src/views/ErrorPage/ErrorPage.tsx +++ b/frontend/src/views/ErrorPage/ErrorPage.tsx @@ -1,11 +1,11 @@ import React from "react"; -import "./ErrorPage.css"; +import { ErrorMessage } from "./styles"; function ErrorPage() { return ( -

+ Oops! This is awkward... You are looking for something that doesn't actually exists. -

+ ); } diff --git a/frontend/src/views/ErrorPage/ErrorPage.css b/frontend/src/views/ErrorPage/styles.ts similarity index 50% rename from frontend/src/views/ErrorPage/ErrorPage.css rename to frontend/src/views/ErrorPage/styles.ts index 3dadd669f..23e2ce164 100644 --- a/frontend/src/views/ErrorPage/ErrorPage.css +++ b/frontend/src/views/ErrorPage/styles.ts @@ -1,6 +1,8 @@ -.error { +import styled from "styled-components"; + +export const ErrorMessage = styled.h1` margin: auto; margin-top: 10%; max-width: 50%; text-align: center; -} +`; diff --git a/frontend/src/views/RegisterPage/RegisterPage.css b/frontend/src/views/RegisterPage/RegisterPage.css deleted file mode 100644 index e69de29bb..000000000 From 58098ebd97c4fbf040f3699cd75ff6f938d59d0b Mon Sep 17 00:00:00 2001 From: Francis <44001949+FKD13@users.noreply.github.com> Date: Wed, 30 Mar 2022 13:42:03 +0200 Subject: [PATCH 131/536] Don't link tests --- .github/workflows/backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index dbe878a58..b43bb7704 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -62,7 +62,7 @@ jobs: path: /usr/local/lib/python3.10/site-packages key: venv-${{ runner.os }}-bullseye-3.10.2-v2-${{ hashFiles('**/poetry.lock') }} - - run: python -m pylint src tests + - run: python -m pylint src Type: needs: [Test] From e269736763bdf89761225136a877807310f2bd91 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 30 Mar 2022 14:00:16 +0200 Subject: [PATCH 132/536] relocated app.css file removed redirect when invalid uuid --- frontend/src/{css-files => }/App.css | 0 frontend/src/App.tsx | 2 +- frontend/src/components/navbar/NavBarElements.tsx | 2 +- frontend/src/views/RegisterPage/RegisterPage.tsx | 4 ---- 4 files changed, 2 insertions(+), 6 deletions(-) rename frontend/src/{css-files => }/App.css (100%) diff --git a/frontend/src/css-files/App.css b/frontend/src/App.css similarity index 100% rename from frontend/src/css-files/App.css rename to frontend/src/App.css diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 278ed30e4..05c360e6f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import "bootstrap/dist/css/bootstrap.min.css"; -import "./css-files/App.css"; +import "./App.css"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import NavBar from "./components/navbar"; import LoginPage from "./views/LoginPage"; diff --git a/frontend/src/components/navbar/NavBarElements.tsx b/frontend/src/components/navbar/NavBarElements.tsx index 1f76511f6..0712b05de 100644 --- a/frontend/src/components/navbar/NavBarElements.tsx +++ b/frontend/src/components/navbar/NavBarElements.tsx @@ -2,7 +2,7 @@ import styled from "styled-components"; import { NavLink as Link } from "react-router-dom"; import { FaBars } from "react-icons/fa"; -import "../../css-files/App.css"; +import "../../App.css"; export const Nav = styled.nav` background: var(--osoc_blue); diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index 2fcc1662b..b6fb8e637 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -26,10 +26,6 @@ function RegisterPage() { const response = await validateRegistrationUrl("1", uuid); if (response) { setUuid(true); - } else { - setTimeout(() => { - navigate("/*"); - }, 5000); } } if (!validUuid) { From 90a2365e170a2f483ccf7955501ec08bc5ee3de6 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 30 Mar 2022 14:08:41 +0200 Subject: [PATCH 133/536] prettier --- frontend/src/utils/api/validateRegisterLink.ts | 5 ++++- frontend/src/views/RegisterPage/styles.ts | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/utils/api/validateRegisterLink.ts b/frontend/src/utils/api/validateRegisterLink.ts index a5e1a6fba..7f4be598c 100644 --- a/frontend/src/utils/api/validateRegisterLink.ts +++ b/frontend/src/utils/api/validateRegisterLink.ts @@ -5,7 +5,10 @@ import { axiosInstance } from "./api"; * Check if a registration url exists by sending a GET to it, * if it returns a 200 then we know the url is valid. */ -export async function validateRegistrationUrl(edition: string, uuid: string | undefined): Promise { +export async function validateRegistrationUrl( + edition: string, + uuid: string | undefined +): Promise { try { await axiosInstance.get(`/editions/${edition}/invites/${uuid}`); return true; diff --git a/frontend/src/views/RegisterPage/styles.ts b/frontend/src/views/RegisterPage/styles.ts index 3ae125b1f..01f97fd76 100644 --- a/frontend/src/views/RegisterPage/styles.ts +++ b/frontend/src/views/RegisterPage/styles.ts @@ -24,4 +24,3 @@ export const RegisterButton = styled.button` border: none; border-radius: 10px; `; - From f5c9141d1c6fdc9fd8c8898558beced5c993d3f4 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 30 Mar 2022 14:58:40 +0200 Subject: [PATCH 134/536] More styled components for pending page --- .../src/views/PendingPage/PendingPage.css | 34 ------------------- .../src/views/PendingPage/PendingPage.tsx | 23 ++++++++----- frontend/src/views/PendingPage/styles.ts | 25 ++++++++++++++ 3 files changed, 40 insertions(+), 42 deletions(-) create mode 100644 frontend/src/views/PendingPage/styles.ts diff --git a/frontend/src/views/PendingPage/PendingPage.css b/frontend/src/views/PendingPage/PendingPage.css index b2a56fe3d..387c6e4bd 100644 --- a/frontend/src/views/PendingPage/PendingPage.css +++ b/frontend/src/views/PendingPage/PendingPage.css @@ -1,37 +1,3 @@ -/* -PendingPage - */ - -.pending-page-content-container { - height: fit-content; - position: absolute; - text-align: center; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} - -.pending-page-content-container span { - font-size: 40px; - display: block; - position: relative; -} - -.pending-page-content { - max-width: 1200px; - text-align: center; - height: fit-content; -} - -.pending-text-container { - width: fit-content; - display: inline-block; -} - -.pending-text-content { - display: flex; -} - .pending-dot-1 { animation: visible-switch-1 2s infinite linear; } diff --git a/frontend/src/views/PendingPage/PendingPage.tsx b/frontend/src/views/PendingPage/PendingPage.tsx index 313167da5..b9e937f43 100644 --- a/frontend/src/views/PendingPage/PendingPage.tsx +++ b/frontend/src/views/PendingPage/PendingPage.tsx @@ -1,24 +1,31 @@ import React from "react"; import "./PendingPage.css"; +import { + PendingPageContainer, + PendingContainer, + PendingTextContainer, + PendingText, +} from "./styles"; + function PendingPage() { return (
-
-
-
-
+ + + +

Your request is pending

.

.

.

-
-
+ +

Please wait until an admin approves your request!

-
-
+ +
); } diff --git a/frontend/src/views/PendingPage/styles.ts b/frontend/src/views/PendingPage/styles.ts new file mode 100644 index 000000000..bd287800a --- /dev/null +++ b/frontend/src/views/PendingPage/styles.ts @@ -0,0 +1,25 @@ +import styled from "styled-components"; + +export const PendingPageContainer = styled.div` + height: fit-content; + position: absolute; + text-align: center; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +`; + +export const PendingContainer = styled.div` + max-width: 1200px; + text-align: center; + height: fit-content; +`; + +export const PendingTextContainer = styled.div` + width: fit-content; + display: inline-block; +`; + +export const PendingText = styled.div` + display: flex; +`; From 218f70099de8504100432ab4cfaa635f5dda41a4 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 30 Mar 2022 14:59:53 +0200 Subject: [PATCH 135/536] Removed inline style for project page --- frontend/src/views/ProjectsPage/ProjectsPage.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index ad2716db1..d6469892f 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -2,18 +2,7 @@ import React from "react"; import "./ProjectsPage.css"; function ProjectPage() { - return ( -
- This is the projects page -
- ); + return
This is the projects page
; } export default ProjectPage; From a7415e93db299472ff04f8d6574d0e0c97d66c51 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 30 Mar 2022 15:01:36 +0200 Subject: [PATCH 136/536] typo in errorpage --- frontend/src/views/ErrorPage/ErrorPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/ErrorPage/ErrorPage.tsx b/frontend/src/views/ErrorPage/ErrorPage.tsx index 5b2679219..74be07bdf 100644 --- a/frontend/src/views/ErrorPage/ErrorPage.tsx +++ b/frontend/src/views/ErrorPage/ErrorPage.tsx @@ -4,7 +4,7 @@ import { ErrorMessage } from "./styles"; function ErrorPage() { return ( - Oops! This is awkward... You are looking for something that doesn't actually exists. + Oops! This is awkward... You are looking for something that doesn't actually exist. ); } From 57d7a78c4be9415a121918bc72b6dde930f8d57b Mon Sep 17 00:00:00 2001 From: beguille Date: Wed, 30 Mar 2022 15:09:32 +0200 Subject: [PATCH 137/536] requested changes by stijn done --- backend/src/app/logic/projects.py | 21 +++++-------------- backend/src/app/routers/editions/editions.py | 2 +- backend/src/app/schemas/projects.py | 7 +++++-- .../test_students/test_students.py | 4 +++- 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index dc2b843d2..fbaadf8a1 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -42,24 +42,13 @@ def logic_get_conflicts(db: Session, edition: Edition) -> ConflictStudentList: conflicts = db_get_conflict_students(db, edition) conflicts_model = [] for student, projects in conflicts: - student_model = Student(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, - decision=student.decision, - wants_to_be_student_coach=student.wants_to_be_student_coach, - edition_name=edition.name) projects_model = [] for project in projects: - project_model = Project(project_id=project.project_id, name=project.name, - number_of_students=project.number_of_students, - edition_name=edition.name, coaches=project.coaches, skills=project.skills, - partners=project.partners, project_roles=project.project_roles) + project_model = (project.project_id, project.name) + print(type(project.project_id)) projects_model.append(project_model) - conflicts_model.append(ConflictStudent(student=student_model, projects=projects_model)) + conflicts_model.append(ConflictStudent(student_id=student.student_id, student_first_name=student.first_name, + student_last_name=student.last_name, projects=projects_model)) - return ConflictStudentList(conflict_students=conflicts_model) + return ConflictStudentList(conflict_students=conflicts_model, edition_name=edition.name) diff --git a/backend/src/app/routers/editions/editions.py b/backend/src/app/routers/editions/editions.py index 1df3a5470..c9a300d6d 100644 --- a/backend/src/app/routers/editions/editions.py +++ b/backend/src/app/routers/editions/editions.py @@ -52,7 +52,7 @@ async def get_edition_by_name(edition_name: str, db: Session = Depends(get_sessi """Get a specific edition. Args: - edition_name (str): the id of the edition that you want to get. + 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). Returns: diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index 9696ead77..bac82b756 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -91,13 +91,16 @@ class ProjectList(CamelCaseModel): class ConflictStudent(CamelCaseModel): """A student together with the projects they are causing a conflict for""" - student: Student - projects: list[Project] + student_first_name: str + student_last_name: str + student_id: int + projects: list[tuple[int, str]] class ConflictStudentList(CamelCaseModel): """A list of ConflictStudents""" conflict_students: list[ConflictStudent] + edition_name: str class InputProject(BaseModel): 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 be4bee48b..aa738c51e 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 @@ -279,6 +279,8 @@ def test_get_conflicts(database_with_data: Session, test_client: TestClient): """test get the conflicts""" response = test_client.get("/editions/ed2022/projects/conflicts") json = response.json() + print(json) assert len(json['conflictStudents']) == 1 - assert json['conflictStudents'][0]['student']['studentId'] == 1 + assert json['conflictStudents'][0]['studentId'] == 1 assert len(json['conflictStudents'][0]['projects']) == 2 + assert json['editionName'] == "ed2022" From 39cd880f32fa4f35297aa03935218cabfad6e204 Mon Sep 17 00:00:00 2001 From: Bert Guillemyn Date: Thu, 31 Mar 2022 09:32:10 +0200 Subject: [PATCH 138/536] Update backend/src/app/logic/projects.py Co-authored-by: Stijn De Clercq <60451863+stijndcl@users.noreply.github.com> --- backend/src/app/logic/projects.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index fbaadf8a1..95a9e7ed9 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -45,7 +45,6 @@ def logic_get_conflicts(db: Session, edition: Edition) -> ConflictStudentList: projects_model = [] for project in projects: project_model = (project.project_id, project.name) - print(type(project.project_id)) projects_model.append(project_model) conflicts_model.append(ConflictStudent(student_id=student.student_id, student_first_name=student.first_name, From 53389d35bf5fa992bc93a95678ff178f2e68145d Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 26 Mar 2022 12:18:48 +0100 Subject: [PATCH 139/536] refactor navbar files --- frontend/src/App.tsx | 2 +- frontend/src/components/navbar/{index.tsx => NavBar.tsx} | 1 + frontend/src/components/navbar/index.ts | 1 + frontend/src/components/navbar/navbar.css | 7 +++++++ frontend/src/css-files/App.css | 6 ------ 5 files changed, 10 insertions(+), 7 deletions(-) rename frontend/src/components/navbar/{index.tsx => NavBar.tsx} (98%) create mode 100644 frontend/src/components/navbar/index.ts create mode 100644 frontend/src/components/navbar/navbar.css diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 435b7b43e..380136452 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import "bootstrap/dist/css/bootstrap.min.css"; import "./css-files/App.css"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; -import NavBar from "./components/navbar"; +import NavBar from "./components/NavBar"; import LoginPage from "./views/LoginPage/LoginPage"; import Students from "./views/Students"; import Users from "./views/Users"; diff --git a/frontend/src/components/navbar/index.tsx b/frontend/src/components/navbar/NavBar.tsx similarity index 98% rename from frontend/src/components/navbar/index.tsx rename to frontend/src/components/navbar/NavBar.tsx index 4c9983bee..7743f14ac 100644 --- a/frontend/src/components/navbar/index.tsx +++ b/frontend/src/components/navbar/NavBar.tsx @@ -1,5 +1,6 @@ import React from "react"; import { Nav, NavLink, Bars, NavMenu } from "./NavBarElementss"; +import "./navbar.css"; function NavBar({ token }: any, { setToken }: any) { let hidden = "nav-hidden"; diff --git a/frontend/src/components/navbar/index.ts b/frontend/src/components/navbar/index.ts new file mode 100644 index 000000000..79cca0c6f --- /dev/null +++ b/frontend/src/components/navbar/index.ts @@ -0,0 +1 @@ +export { default } from "./NavBar" \ No newline at end of file diff --git a/frontend/src/components/navbar/navbar.css b/frontend/src/components/navbar/navbar.css new file mode 100644 index 000000000..f3170af8d --- /dev/null +++ b/frontend/src/components/navbar/navbar.css @@ -0,0 +1,7 @@ +.nav-links { + display: flex; +} + +.nav-hidden { + visibility: hidden; +} \ No newline at end of file diff --git a/frontend/src/css-files/App.css b/frontend/src/css-files/App.css index ee150fcac..9a1d6988f 100644 --- a/frontend/src/css-files/App.css +++ b/frontend/src/css-files/App.css @@ -12,13 +12,7 @@ height: 100%; } -.nav-links { - display: flex; -} -.nav-hidden { - visibility: hidden; -} * { box-sizing: border-box; From 7d87b316df5744bd5ff4f7656d3f675667c07bf0 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 26 Mar 2022 12:35:07 +0100 Subject: [PATCH 140/536] refactor error page --- frontend/src/views/ErrorPage.tsx | 7 ------- frontend/src/views/ErrorPage/ErrorPage.css | 6 ++++++ frontend/src/views/ErrorPage/ErrorPage.tsx | 12 ++++++++++++ 3 files changed, 18 insertions(+), 7 deletions(-) delete mode 100644 frontend/src/views/ErrorPage.tsx create mode 100644 frontend/src/views/ErrorPage/ErrorPage.css create mode 100644 frontend/src/views/ErrorPage/ErrorPage.tsx diff --git a/frontend/src/views/ErrorPage.tsx b/frontend/src/views/ErrorPage.tsx deleted file mode 100644 index 126863917..000000000 --- a/frontend/src/views/ErrorPage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -function ErrorPage() { - return

404: Page not found

; -} - -export default ErrorPage; diff --git a/frontend/src/views/ErrorPage/ErrorPage.css b/frontend/src/views/ErrorPage/ErrorPage.css new file mode 100644 index 000000000..3dadd669f --- /dev/null +++ b/frontend/src/views/ErrorPage/ErrorPage.css @@ -0,0 +1,6 @@ +.error { + margin: auto; + margin-top: 10%; + max-width: 50%; + text-align: center; +} diff --git a/frontend/src/views/ErrorPage/ErrorPage.tsx b/frontend/src/views/ErrorPage/ErrorPage.tsx new file mode 100644 index 000000000..e82b5ad8a --- /dev/null +++ b/frontend/src/views/ErrorPage/ErrorPage.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import "./ErrorPage.css"; + +function ErrorPage() { + return ( +

+ Oops! This is awkward... You are looking for something that doesn't actually exists. +

+ ); +} + +export default ErrorPage; From b3e1f339a24b24cda6aa10c828767d9d570e5b04 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 26 Mar 2022 14:03:46 +0100 Subject: [PATCH 141/536] every page has its directory and index file --- frontend/src/App.tsx | 16 +-- frontend/src/css-files/LogInButtons.css | 48 -------- frontend/src/views/ErrorPage/index.ts | 1 + frontend/src/views/Home.tsx | 18 --- frontend/src/views/LoginPage/index.ts | 1 + frontend/src/views/PendingPage/index.ts | 1 + .../src/views/ProjectsPage/ProjectsPage.css | 0 .../views/{ => ProjectsPage}/ProjectsPage.tsx | 1 + frontend/src/views/ProjectsPage/index.ts | 1 + .../src/views/RegisterPage/RegisterPage.css | 0 .../src/views/RegisterPage/RegisterPage.tsx | 112 ++++++++++++++++++ frontend/src/views/RegisterPage/index.ts | 1 + .../src/views/StudentsPage/StudentsPage.css | 0 .../StudentsPage.tsx} | 1 + frontend/src/views/StudentsPage/index.ts | 1 + frontend/src/views/UsersPage/UsersPage.css | 0 .../{Users.tsx => UsersPage/UsersPage.tsx} | 0 frontend/src/views/UsersPage/index.ts | 1 + 18 files changed, 129 insertions(+), 74 deletions(-) delete mode 100644 frontend/src/css-files/LogInButtons.css create mode 100644 frontend/src/views/ErrorPage/index.ts delete mode 100644 frontend/src/views/Home.tsx create mode 100644 frontend/src/views/LoginPage/index.ts create mode 100644 frontend/src/views/PendingPage/index.ts create mode 100644 frontend/src/views/ProjectsPage/ProjectsPage.css rename frontend/src/views/{ => ProjectsPage}/ProjectsPage.tsx (92%) create mode 100644 frontend/src/views/ProjectsPage/index.ts create mode 100644 frontend/src/views/RegisterPage/RegisterPage.css create mode 100644 frontend/src/views/RegisterPage/RegisterPage.tsx create mode 100644 frontend/src/views/RegisterPage/index.ts create mode 100644 frontend/src/views/StudentsPage/StudentsPage.css rename frontend/src/views/{Students.tsx => StudentsPage/StudentsPage.tsx} (81%) create mode 100644 frontend/src/views/StudentsPage/index.ts create mode 100644 frontend/src/views/UsersPage/UsersPage.css rename frontend/src/views/{Users.tsx => UsersPage/UsersPage.tsx} (100%) create mode 100644 frontend/src/views/UsersPage/index.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 380136452..2780310bd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,14 +2,14 @@ import React, { useState } from "react"; import "bootstrap/dist/css/bootstrap.min.css"; import "./css-files/App.css"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; -import NavBar from "./components/NavBar"; -import LoginPage from "./views/LoginPage/LoginPage"; -import Students from "./views/Students"; -import Users from "./views/Users"; -import ProjectsPage from "./views/ProjectsPage"; -import RegisterForm from "./views/RegisterForm/RegisterForm"; -import ErrorPage from "./views/ErrorPage"; -import PendingPage from "./views/PendingPage/PendingPage"; +import NavBar from "./components/navbar"; +import LoginPage from "./views/LoginPage"; +import Students from "./views/StudentsPage/StudentsPage"; +import Users from "./views/UsersPage/UsersPage"; +import ProjectsPage from "./views/ProjectsPage/ProjectsPage"; +import RegisterForm from "./views/RegisterPage/RegisterPage"; +import ErrorPage from "./views/ErrorPage/ErrorPage"; +import PendingPage from "./views/PendingPage"; import Footer from "./components/Footer"; import { Container, ContentWrapper } from "./app.styles"; diff --git a/frontend/src/css-files/LogInButtons.css b/frontend/src/css-files/LogInButtons.css deleted file mode 100644 index b4bbf1fc4..000000000 --- a/frontend/src/css-files/LogInButtons.css +++ /dev/null @@ -1,48 +0,0 @@ -.login-buttons { - width: available; - margin-top: 60px; - display: flex; - justify-content: center; -} - -.login-button { - display: block; - position: relative; - width: 280px; - height: 40px; - font-size: 25px; - padding: 0.2rem; - background: white; - cursor: pointer; - color: var(--react_dark_grey); - border-radius: 10px; - border-color: black; - border-width: 2px; -} - -.login-button-content { - display: flex; - align-items: center; - margin-left: 10px; -} - -.email-login { - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 10px; -} - -.google-login { - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 10px; -} - -.github-login { - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 10px; -} diff --git a/frontend/src/views/ErrorPage/index.ts b/frontend/src/views/ErrorPage/index.ts new file mode 100644 index 000000000..4a1ddb1bf --- /dev/null +++ b/frontend/src/views/ErrorPage/index.ts @@ -0,0 +1 @@ +export { default } from "./ErrorPage"; diff --git a/frontend/src/views/Home.tsx b/frontend/src/views/Home.tsx deleted file mode 100644 index 7c770aa74..000000000 --- a/frontend/src/views/Home.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; - -function Home() { - return ( -
-

Home

-
- ); -} - -export default Home; diff --git a/frontend/src/views/LoginPage/index.ts b/frontend/src/views/LoginPage/index.ts new file mode 100644 index 000000000..f81523088 --- /dev/null +++ b/frontend/src/views/LoginPage/index.ts @@ -0,0 +1 @@ +export { default } from "./LoginPage"; diff --git a/frontend/src/views/PendingPage/index.ts b/frontend/src/views/PendingPage/index.ts new file mode 100644 index 000000000..72abd154f --- /dev/null +++ b/frontend/src/views/PendingPage/index.ts @@ -0,0 +1 @@ +export { default } from "./PendingPage"; diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.css b/frontend/src/views/ProjectsPage/ProjectsPage.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/views/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx similarity index 92% rename from frontend/src/views/ProjectsPage.tsx rename to frontend/src/views/ProjectsPage/ProjectsPage.tsx index a335327f1..ad2716db1 100644 --- a/frontend/src/views/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -1,4 +1,5 @@ import React from "react"; +import "./ProjectsPage.css"; function ProjectPage() { return ( diff --git a/frontend/src/views/ProjectsPage/index.ts b/frontend/src/views/ProjectsPage/index.ts new file mode 100644 index 000000000..7b601b450 --- /dev/null +++ b/frontend/src/views/ProjectsPage/index.ts @@ -0,0 +1 @@ +export { default } from "./ProjectsPage"; diff --git a/frontend/src/views/RegisterPage/RegisterPage.css b/frontend/src/views/RegisterPage/RegisterPage.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx new file mode 100644 index 000000000..719e50edb --- /dev/null +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -0,0 +1,112 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { axiosInstance } from "../../utils/api/api"; +import OSOCLetters from "../../components/OSOCLetters/OSOCLetters"; +import "./RegisterPage.css"; + +import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; + +function RegisterForm() { + function register() { + // Check if passwords are the same + if (password !== confirmPassword) { + alert("Passwords do not match"); + return; + } + // Basic email checker + if (!/^\w+([\\.-]?\w+-)*@\w+([\\.-]?\w+)*(\.\w{2,3})+$/.test(email)) { + alert("This is not a valid email"); + return; + } + + // TODO this has to change to get the edition the invite belongs to + const edition = "2022"; + const payload = new FormData(); + payload.append("username", email); + payload.append("name", name); + payload.append("password", password); + payload.append("confirmPassword", confirmPassword); + + axiosInstance + .post("/editions/" + edition + "/register/email", payload) + .then((response: any) => console.log(response)) + .then(() => navigate("/pending")) + .catch(function (error: any) { + console.log(error); + }); + } + + const [email, setEmail] = useState(""); + const [name, setName] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + + const navigate = useNavigate(); + + return ( +
+
+ +

Create an account

+
+ Sign up with your social media account or email address +
+
+
+ +
+ +
+
+ +

or

+ +
+
+ setEmail(e.target.value)} + /> +
+
+ setName(e.target.value)} + /> +
+
+ setPassword(e.target.value)} + /> +
+
+ setConfirmPassword(e.target.value)} + /> +
+
+
+ +
+
+
+ ); +} + +export default RegisterForm; diff --git a/frontend/src/views/RegisterPage/index.ts b/frontend/src/views/RegisterPage/index.ts new file mode 100644 index 000000000..926657ec0 --- /dev/null +++ b/frontend/src/views/RegisterPage/index.ts @@ -0,0 +1 @@ +export { default } from "./RegisterPage"; diff --git a/frontend/src/views/StudentsPage/StudentsPage.css b/frontend/src/views/StudentsPage/StudentsPage.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/views/Students.tsx b/frontend/src/views/StudentsPage/StudentsPage.tsx similarity index 81% rename from frontend/src/views/Students.tsx rename to frontend/src/views/StudentsPage/StudentsPage.tsx index d1a6b4009..162122f17 100644 --- a/frontend/src/views/Students.tsx +++ b/frontend/src/views/StudentsPage/StudentsPage.tsx @@ -1,4 +1,5 @@ import React from "react"; +import "./StudentsPage.css" function Students() { return
This is the students page
; diff --git a/frontend/src/views/StudentsPage/index.ts b/frontend/src/views/StudentsPage/index.ts new file mode 100644 index 000000000..7bcdecdbb --- /dev/null +++ b/frontend/src/views/StudentsPage/index.ts @@ -0,0 +1 @@ +export { default } from "./StudentsPage"; diff --git a/frontend/src/views/UsersPage/UsersPage.css b/frontend/src/views/UsersPage/UsersPage.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/views/Users.tsx b/frontend/src/views/UsersPage/UsersPage.tsx similarity index 100% rename from frontend/src/views/Users.tsx rename to frontend/src/views/UsersPage/UsersPage.tsx diff --git a/frontend/src/views/UsersPage/index.ts b/frontend/src/views/UsersPage/index.ts new file mode 100644 index 000000000..6f0687e25 --- /dev/null +++ b/frontend/src/views/UsersPage/index.ts @@ -0,0 +1 @@ +export { default } from "./UsersPage"; From 27c58f4cbc108c41f4653ad94cf56bdbb6cd62b4 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 26 Mar 2022 14:07:11 +0100 Subject: [PATCH 142/536] changed some imports --- frontend/src/App.tsx | 6 +++--- frontend/src/components/OSOCLetters/index.ts | 1 + frontend/src/views/RegisterPage/RegisterPage.tsx | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/OSOCLetters/index.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2780310bd..8fadf2ed6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,10 +4,10 @@ import "./css-files/App.css"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import NavBar from "./components/navbar"; import LoginPage from "./views/LoginPage"; -import Students from "./views/StudentsPage/StudentsPage"; +import Students from "./views/StudentsPage"; import Users from "./views/UsersPage/UsersPage"; -import ProjectsPage from "./views/ProjectsPage/ProjectsPage"; -import RegisterForm from "./views/RegisterPage/RegisterPage"; +import ProjectsPage from "./views/ProjectsPage"; +import RegisterForm from "./views/RegisterPage"; import ErrorPage from "./views/ErrorPage/ErrorPage"; import PendingPage from "./views/PendingPage"; import Footer from "./components/Footer"; diff --git a/frontend/src/components/OSOCLetters/index.ts b/frontend/src/components/OSOCLetters/index.ts new file mode 100644 index 000000000..4d35943ea --- /dev/null +++ b/frontend/src/components/OSOCLetters/index.ts @@ -0,0 +1 @@ +export { default } from "./OSOCLetters"; diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index 719e50edb..03c08a969 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { axiosInstance } from "../../utils/api/api"; -import OSOCLetters from "../../components/OSOCLetters/OSOCLetters"; +import OSOCLetters from "../../components/OSOCLetters"; import "./RegisterPage.css"; import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; From d04b39360b76a7844463b8773fc960e74e367490 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 26 Mar 2022 14:22:54 +0100 Subject: [PATCH 143/536] cleaned up App.css file --- frontend/src/App.tsx | 2 +- frontend/src/components/navbar/NavBar.tsx | 2 +- ...{NavBarElementss.tsx => NavBarElements.tsx} | 0 frontend/src/components/navbar/index.ts | 2 +- frontend/src/components/navbar/navbar.css | 11 ++++++++++- frontend/src/css-files/App.css | 18 ------------------ frontend/src/views/LoginPage/LoginPage.css | 7 +++++++ .../src/views/StudentsPage/StudentsPage.tsx | 2 +- .../UsersPage/{UsersPage.tsx => Users.tsx} | 0 frontend/src/views/UsersPage/index.ts | 2 +- 10 files changed, 22 insertions(+), 24 deletions(-) rename frontend/src/components/navbar/{NavBarElementss.tsx => NavBarElements.tsx} (100%) rename frontend/src/views/UsersPage/{UsersPage.tsx => Users.tsx} (100%) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8fadf2ed6..525a28b75 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,7 +5,7 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import NavBar from "./components/navbar"; import LoginPage from "./views/LoginPage"; import Students from "./views/StudentsPage"; -import Users from "./views/UsersPage/UsersPage"; +import Users from "./views/UsersPage/Users"; import ProjectsPage from "./views/ProjectsPage"; import RegisterForm from "./views/RegisterPage"; import ErrorPage from "./views/ErrorPage/ErrorPage"; diff --git a/frontend/src/components/navbar/NavBar.tsx b/frontend/src/components/navbar/NavBar.tsx index 7743f14ac..5bc2cc2a0 100644 --- a/frontend/src/components/navbar/NavBar.tsx +++ b/frontend/src/components/navbar/NavBar.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Nav, NavLink, Bars, NavMenu } from "./NavBarElementss"; +import { Nav, NavLink, Bars, NavMenu } from "./NavBarElements"; import "./navbar.css"; function NavBar({ token }: any, { setToken }: any) { diff --git a/frontend/src/components/navbar/NavBarElementss.tsx b/frontend/src/components/navbar/NavBarElements.tsx similarity index 100% rename from frontend/src/components/navbar/NavBarElementss.tsx rename to frontend/src/components/navbar/NavBarElements.tsx diff --git a/frontend/src/components/navbar/index.ts b/frontend/src/components/navbar/index.ts index 79cca0c6f..3ab07fb62 100644 --- a/frontend/src/components/navbar/index.ts +++ b/frontend/src/components/navbar/index.ts @@ -1 +1 @@ -export { default } from "./NavBar" \ No newline at end of file +export { default } from "./NavBar"; diff --git a/frontend/src/components/navbar/navbar.css b/frontend/src/components/navbar/navbar.css index f3170af8d..36c48b56c 100644 --- a/frontend/src/components/navbar/navbar.css +++ b/frontend/src/components/navbar/navbar.css @@ -4,4 +4,13 @@ .nav-hidden { visibility: hidden; -} \ No newline at end of file +} + +.logo-plus-name { + color: #fff; + display: flex; + align-items: center; + text-decoration: none; + padding: 0 1rem; + height: 100%; +} diff --git a/frontend/src/css-files/App.css b/frontend/src/css-files/App.css index 9a1d6988f..bee7b3085 100644 --- a/frontend/src/css-files/App.css +++ b/frontend/src/css-files/App.css @@ -12,8 +12,6 @@ height: 100%; } - - * { box-sizing: border-box; margin: 0; @@ -24,19 +22,3 @@ body { background-color: var(--background_color); color: white; } - -.welcome-text { - max-width: 800px; - text-align: center; - justify-content: center; - margin-bottom: 50px; -} - -.logo-plus-name { - color: #fff; - display: flex; - align-items: center; - text-decoration: none; - padding: 0 1rem; - height: 100%; -} diff --git a/frontend/src/views/LoginPage/LoginPage.css b/frontend/src/views/LoginPage/LoginPage.css index c9f495933..0376a8286 100644 --- a/frontend/src/views/LoginPage/LoginPage.css +++ b/frontend/src/views/LoginPage/LoginPage.css @@ -6,6 +6,13 @@ flex-direction: column; } +.welcome-text { + max-width: 800px; + text-align: center; + justify-content: center; + margin-bottom: 50px; +} + .socials-container { display: flex; justify-content: center; diff --git a/frontend/src/views/StudentsPage/StudentsPage.tsx b/frontend/src/views/StudentsPage/StudentsPage.tsx index 162122f17..5a158d93d 100644 --- a/frontend/src/views/StudentsPage/StudentsPage.tsx +++ b/frontend/src/views/StudentsPage/StudentsPage.tsx @@ -1,5 +1,5 @@ import React from "react"; -import "./StudentsPage.css" +import "./StudentsPage.css"; function Students() { return
This is the students page
; diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/Users.tsx similarity index 100% rename from frontend/src/views/UsersPage/UsersPage.tsx rename to frontend/src/views/UsersPage/Users.tsx diff --git a/frontend/src/views/UsersPage/index.ts b/frontend/src/views/UsersPage/index.ts index 6f0687e25..d25de51bd 100644 --- a/frontend/src/views/UsersPage/index.ts +++ b/frontend/src/views/UsersPage/index.ts @@ -1 +1 @@ -export { default } from "./UsersPage"; +export { default } from "./Users"; From aa68a6e4f25621f193a558496ea05d61a62753be Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 26 Mar 2022 14:40:58 +0100 Subject: [PATCH 144/536] import update --- frontend/src/App.tsx | 4 ++-- frontend/src/views/UsersPage/{Users.tsx => UsersPage.tsx} | 0 frontend/src/views/UsersPage/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename frontend/src/views/UsersPage/{Users.tsx => UsersPage.tsx} (100%) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 525a28b75..bcde7d05c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,10 +5,10 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import NavBar from "./components/navbar"; import LoginPage from "./views/LoginPage"; import Students from "./views/StudentsPage"; -import Users from "./views/UsersPage/Users"; +import Users from "./views/UsersPage"; import ProjectsPage from "./views/ProjectsPage"; import RegisterForm from "./views/RegisterPage"; -import ErrorPage from "./views/ErrorPage/ErrorPage"; +import ErrorPage from "./views/ErrorPage"; import PendingPage from "./views/PendingPage"; import Footer from "./components/Footer"; import { Container, ContentWrapper } from "./app.styles"; diff --git a/frontend/src/views/UsersPage/Users.tsx b/frontend/src/views/UsersPage/UsersPage.tsx similarity index 100% rename from frontend/src/views/UsersPage/Users.tsx rename to frontend/src/views/UsersPage/UsersPage.tsx diff --git a/frontend/src/views/UsersPage/index.ts b/frontend/src/views/UsersPage/index.ts index d25de51bd..6f0687e25 100644 --- a/frontend/src/views/UsersPage/index.ts +++ b/frontend/src/views/UsersPage/index.ts @@ -1 +1 @@ -export { default } from "./Users"; +export { default } from "./UsersPage"; From c797faa5ef43b34c931f26f592563e417fe51289 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 26 Mar 2022 16:24:08 +0100 Subject: [PATCH 145/536] moved login axios code to utils/api --- frontend/src/App.tsx | 10 ++++---- frontend/src/utils/api/login.ts | 20 +++++++++++++++ frontend/src/views/LoginPage/LoginPage.tsx | 30 ++++++++-------------- frontend/src/views/UsersPage/UsersPage.tsx | 4 +-- 4 files changed, 37 insertions(+), 27 deletions(-) create mode 100644 frontend/src/utils/api/login.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bcde7d05c..3fc75a0ca 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,8 +4,8 @@ import "./css-files/App.css"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import NavBar from "./components/navbar"; import LoginPage from "./views/LoginPage"; -import Students from "./views/StudentsPage"; -import Users from "./views/UsersPage"; +import StudentsPage from "./views/StudentsPage"; +import UsersPage from "./views/UsersPage"; import ProjectsPage from "./views/ProjectsPage"; import RegisterForm from "./views/RegisterPage"; import ErrorPage from "./views/ErrorPage"; @@ -24,9 +24,9 @@ function App() { } /> - } /> - } /> - } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/frontend/src/utils/api/login.ts b/frontend/src/utils/api/login.ts new file mode 100644 index 000000000..3717e96b5 --- /dev/null +++ b/frontend/src/utils/api/login.ts @@ -0,0 +1,20 @@ +import axios from "axios"; +import { axiosInstance } from "./api"; + +export async function logIn({ setToken }: any, email: any, password: any) { + const payload = new FormData(); + payload.append("username", email); + payload.append("password", password); + try { + await axiosInstance.post("/login/token", payload).then((response: any) => { + setToken(response.data.accessToken); + }); + return true; + } catch (error) { + if (axios.isAxiosError(error)) { + return false; + } else { + throw error; + } + } +} diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index 10bd08078..73f4cfc9a 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -1,31 +1,22 @@ import { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { axiosInstance } from "../../utils/api/api"; import "./LoginPage.css"; +import { logIn } from "../../utils/api/login"; +import { useNavigate } from "react-router-dom"; import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; function LoginPage({ setToken }: any) { - function logIn() { - const payload = new FormData(); - payload.append("username", email); - payload.append("password", password); - - axiosInstance - .post("/login/token", payload) - .then((response: any) => { - setToken(response.data.accessToken); - }) - .then(() => navigate("/students")) - .catch(function (error: any) { - console.log(error); - }); - } const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); - const navigate = useNavigate(); + function callLogIn() { + logIn({ setToken }, email, password).then(response => { + if (response) navigate("/students"); + else alert("Login failed"); + }); + } + return (
@@ -47,7 +38,6 @@ function LoginPage({ setToken }: any) {
-
-
diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index 77d2905c0..b96c7fc57 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -1,7 +1,7 @@ import React from "react"; -function Users() { +function UsersPage() { return
This is the users page
; } -export default Users; +export default UsersPage; From 666f5c93350b3e71b470092c7c1100b2d191d46b Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 26 Mar 2022 16:35:12 +0100 Subject: [PATCH 146/536] merged with master --- frontend/src/App.tsx | 4 +- .../src/views/RegisterForm/RegisterForm.tsx | 127 --------------- .../src/views/RegisterPage/RegisterPage.tsx | 151 ++++++++++-------- 3 files changed, 85 insertions(+), 197 deletions(-) delete mode 100644 frontend/src/views/RegisterForm/RegisterForm.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3fc75a0ca..278ed30e4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,7 +7,7 @@ import LoginPage from "./views/LoginPage"; import StudentsPage from "./views/StudentsPage"; import UsersPage from "./views/UsersPage"; import ProjectsPage from "./views/ProjectsPage"; -import RegisterForm from "./views/RegisterPage"; +import RegisterPage from "./views/RegisterPage"; import ErrorPage from "./views/ErrorPage"; import PendingPage from "./views/PendingPage"; import Footer from "./components/Footer"; @@ -24,7 +24,7 @@ function App() { } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/views/RegisterForm/RegisterForm.tsx b/frontend/src/views/RegisterForm/RegisterForm.tsx deleted file mode 100644 index 9179c33ac..000000000 --- a/frontend/src/views/RegisterForm/RegisterForm.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; -import { axiosInstance } from "../../utils/api/api"; - -import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; - -interface RegisterFields { - email: string; - name: string; - uuid: string; - pw: string; -} - -function RegisterForm() { - function register(uuid: string) { - // Check if passwords are the same - if (password !== confirmPassword) { - alert("Passwords do not match"); - return; - } - // Basic email checker - if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { - alert("This is not a valid email"); - return; - } - - // TODO this has to change to get the edition the invite belongs to - const edition = "1"; - const payload: RegisterFields = { email: email, name: name, uuid: uuid, pw: password }; - - axiosInstance - .post("/editions/" + edition + "/register/email", payload) - .then((response: any) => console.log(response)) - .then(() => navigate("/pending")) - .catch(function (error: any) { - console.log(error); - }); - } - - const [email, setEmail] = useState(""); - const [name, setName] = useState(""); - const [password, setPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - - const navigate = useNavigate(); - - const params = useParams(); - const uuid = params.uuid; - - const [validUuid, setUuid] = useState(false); - - axiosInstance.get("/editions/" + 1 + "/invites/" + uuid).then(response => { - if (response.data.uuid === uuid) { - setUuid(true); - } - }); - - if (validUuid && uuid) { - return ( -
-
-

Create an account

- -
- Sign up with your social media account or email address. Your unique link is - not useable again ({uuid}) -
-
-
- -
- -
-
- -

or

- -
-
- setEmail(e.target.value)} - /> -
-
- setName(e.target.value)} - /> -
-
- setPassword(e.target.value)} - /> -
-
- setConfirmPassword(e.target.value)} - /> -
-
-
- -
-
-
- ); - } else return
Not a valid register url
; -} - -export default RegisterForm; diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index 03c08a969..dc0011ed9 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -1,31 +1,32 @@ import { useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { axiosInstance } from "../../utils/api/api"; -import OSOCLetters from "../../components/OSOCLetters"; -import "./RegisterPage.css"; import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; -function RegisterForm() { - function register() { +interface RegisterFields { + email: string; + name: string; + uuid: string; + pw: string; +} + +function RegisterPage() { + function register(uuid: string) { // Check if passwords are the same if (password !== confirmPassword) { alert("Passwords do not match"); return; } // Basic email checker - if (!/^\w+([\\.-]?\w+-)*@\w+([\\.-]?\w+)*(\.\w{2,3})+$/.test(email)) { + if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { alert("This is not a valid email"); return; } // TODO this has to change to get the edition the invite belongs to - const edition = "2022"; - const payload = new FormData(); - payload.append("username", email); - payload.append("name", name); - payload.append("password", password); - payload.append("confirmPassword", confirmPassword); + const edition = "1"; + const payload: RegisterFields = { email: email, name: name, uuid: uuid, pw: password }; axiosInstance .post("/editions/" + edition + "/register/email", payload) @@ -43,70 +44,84 @@ function RegisterForm() { const navigate = useNavigate(); - return ( -
-
- -

Create an account

-
- Sign up with your social media account or email address -
-
-
- -
- -
-
+ const params = useParams(); + const uuid = params.uuid; -

or

+ const [validUuid, setUuid] = useState(false); -
-
- setEmail(e.target.value)} - /> + axiosInstance.get("/editions/" + 1 + "/invites/" + uuid).then(response => { + if (response.data.uuid === uuid) { + setUuid(true); + } + }); + + if (validUuid && uuid) { + return ( +
+
+

Create an account

+ +
+ Sign up with your social media account or email address. Your unique link is + not useable again ({uuid})
-
- setName(e.target.value)} - /> +
+
+ +
+ +
-
- setPassword(e.target.value)} - /> + +

or

+ +
+
+ setEmail(e.target.value)} + /> +
+
+ setName(e.target.value)} + /> +
+
+ setPassword(e.target.value)} + /> +
+
+ setConfirmPassword(e.target.value)} + /> +
- setConfirmPassword(e.target.value)} - /> +
-
- -
-
- ); + ); + } else return
Not a valid register url
; } -export default RegisterForm; +export default RegisterPage; From 88295b12b3229b84fce15db4f21fa1bd1db3a686 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 27 Mar 2022 15:46:37 +0200 Subject: [PATCH 147/536] Making seperate components for login page --- .../SocialButtons/SocialButtons.tsx | 15 +++++++++++ .../LoginComponents/SocialButtons/index.ts | 1 + .../LoginComponents/SocialButtons/styles.ts | 15 +++++++++++ .../WelcomeText/WelcomeText.tsx | 14 ++++++++++ .../LoginComponents/WelcomeText/index.ts | 1 + .../LoginComponents/WelcomeText/styles.ts | 9 +++++++ frontend/src/utils/api/login.ts | 10 ++++--- frontend/src/views/LoginPage/LoginPage.css | 26 +------------------ frontend/src/views/LoginPage/LoginPage.tsx | 24 +++++------------ frontend/src/views/LoginPage/styles.ts | 7 +++++ 10 files changed, 76 insertions(+), 46 deletions(-) create mode 100644 frontend/src/components/LoginComponents/SocialButtons/SocialButtons.tsx create mode 100644 frontend/src/components/LoginComponents/SocialButtons/index.ts create mode 100644 frontend/src/components/LoginComponents/SocialButtons/styles.ts create mode 100644 frontend/src/components/LoginComponents/WelcomeText/WelcomeText.tsx create mode 100644 frontend/src/components/LoginComponents/WelcomeText/index.ts create mode 100644 frontend/src/components/LoginComponents/WelcomeText/styles.ts create mode 100644 frontend/src/views/LoginPage/styles.ts diff --git a/frontend/src/components/LoginComponents/SocialButtons/SocialButtons.tsx b/frontend/src/components/LoginComponents/SocialButtons/SocialButtons.tsx new file mode 100644 index 000000000..3cca2d539 --- /dev/null +++ b/frontend/src/components/LoginComponents/SocialButtons/SocialButtons.tsx @@ -0,0 +1,15 @@ +import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; +import { SocialsContainer, Socials, GoogleLoginContainer } from "./styles"; + +export default function SocialButtons() { + return ( + + + + + + + + + ); +} diff --git a/frontend/src/components/LoginComponents/SocialButtons/index.ts b/frontend/src/components/LoginComponents/SocialButtons/index.ts new file mode 100644 index 000000000..1aa8d729f --- /dev/null +++ b/frontend/src/components/LoginComponents/SocialButtons/index.ts @@ -0,0 +1 @@ +export { default } from "./SocialButtons"; diff --git a/frontend/src/components/LoginComponents/SocialButtons/styles.ts b/frontend/src/components/LoginComponents/SocialButtons/styles.ts new file mode 100644 index 000000000..8ed2a53d5 --- /dev/null +++ b/frontend/src/components/LoginComponents/SocialButtons/styles.ts @@ -0,0 +1,15 @@ +import styled from "styled-components"; + +export const SocialsContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +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 new file mode 100644 index 000000000..04be7e6c3 --- /dev/null +++ b/frontend/src/components/LoginComponents/WelcomeText/WelcomeText.tsx @@ -0,0 +1,14 @@ +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. +

+
+ ); +} diff --git a/frontend/src/components/LoginComponents/WelcomeText/index.ts b/frontend/src/components/LoginComponents/WelcomeText/index.ts new file mode 100644 index 000000000..e21432cf6 --- /dev/null +++ b/frontend/src/components/LoginComponents/WelcomeText/index.ts @@ -0,0 +1 @@ +export { default } from "./WelcomeText"; diff --git a/frontend/src/components/LoginComponents/WelcomeText/styles.ts b/frontend/src/components/LoginComponents/WelcomeText/styles.ts new file mode 100644 index 000000000..aebc86f01 --- /dev/null +++ b/frontend/src/components/LoginComponents/WelcomeText/styles.ts @@ -0,0 +1,9 @@ +import styled from "styled-components"; + +export const WelcomeTextContainer = styled.div` + max-width: 800px; + text-align: center; + justify-content: center; + margin: auto; + margin-bottom: 50px; +`; diff --git a/frontend/src/utils/api/login.ts b/frontend/src/utils/api/login.ts index 3717e96b5..050c46135 100644 --- a/frontend/src/utils/api/login.ts +++ b/frontend/src/utils/api/login.ts @@ -1,14 +1,18 @@ import axios from "axios"; import { axiosInstance } from "./api"; +interface LoginResponse { + accessToken: string; +} + export async function logIn({ setToken }: any, email: any, password: any) { const payload = new FormData(); payload.append("username", email); payload.append("password", password); try { - await axiosInstance.post("/login/token", payload).then((response: any) => { - setToken(response.data.accessToken); - }); + const response = await axiosInstance.post("/login/token", payload); + const login = response.data as LoginResponse; + await setToken(login.accessToken); return true; } catch (error) { if (axios.isAxiosError(error)) { diff --git a/frontend/src/views/LoginPage/LoginPage.css b/frontend/src/views/LoginPage/LoginPage.css index 0376a8286..158b035d8 100644 --- a/frontend/src/views/LoginPage/LoginPage.css +++ b/frontend/src/views/LoginPage/LoginPage.css @@ -6,28 +6,6 @@ flex-direction: column; } -.welcome-text { - max-width: 800px; - text-align: center; - justify-content: center; - margin-bottom: 50px; -} - -.socials-container { - display: flex; - justify-content: center; - align-items: center; -} - -.socials { - min-width: 230px; - height: fit-content; -} - -.google-login-container { - margin-bottom: 15px; -} - .register-form-content-container { height: fit-content; text-align: center; @@ -88,9 +66,7 @@ input[type="text"] { } .border-right { - margin-right: 20px; - padding-right: 20px; - border-right: 2px solid rgba(182, 182, 182, 0.603); + } .no-account { diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index 73f4cfc9a..f8d679a65 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -3,7 +3,9 @@ import "./LoginPage.css"; import { logIn } from "../../utils/api/login"; import { useNavigate } from "react-router-dom"; -import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; +import SocialButtons from "../../components/LoginComponents/SocialButtons"; +import WelcomeText from "../../components/LoginComponents/WelcomeText"; +import { VerticalDivider } from "./styles"; function LoginPage({ setToken }: any) { const [email, setEmail] = useState(""); @@ -20,24 +22,10 @@ function LoginPage({ setToken }: any) { 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 quick as possible. -

-
+
-
-
-
- -
- -
-
-
+ +
Date: Sun, 27 Mar 2022 17:56:57 +0200 Subject: [PATCH 148/536] make input fields of login page seperate components --- .../InputFields/Email/Email.tsx | 13 +++++++++ .../InputFields/Email/index.ts | 1 + .../InputFields/Password/Password.tsx | 13 +++++++++ .../InputFields/Password/index.ts | 1 + frontend/src/components/navbar/NavBar.tsx | 2 +- frontend/src/views/LoginPage/LoginPage.tsx | 28 ++++++------------- 6 files changed, 37 insertions(+), 21 deletions(-) create mode 100644 frontend/src/components/LoginComponents/InputFields/Email/Email.tsx create mode 100644 frontend/src/components/LoginComponents/InputFields/Email/index.ts create mode 100644 frontend/src/components/LoginComponents/InputFields/Password/Password.tsx create mode 100644 frontend/src/components/LoginComponents/InputFields/Password/index.ts diff --git a/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx b/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx new file mode 100644 index 000000000..726f99432 --- /dev/null +++ b/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx @@ -0,0 +1,13 @@ +export default function Email({ email, setEmail }: any) { + return ( +
+ setEmail(e.target.value)} + /> +
+ ); +} diff --git a/frontend/src/components/LoginComponents/InputFields/Email/index.ts b/frontend/src/components/LoginComponents/InputFields/Email/index.ts new file mode 100644 index 000000000..f2681e1e2 --- /dev/null +++ b/frontend/src/components/LoginComponents/InputFields/Email/index.ts @@ -0,0 +1 @@ +export { default } from "./Email"; diff --git a/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx b/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx new file mode 100644 index 000000000..5d30b43ba --- /dev/null +++ b/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx @@ -0,0 +1,13 @@ +export default function Password({password, setPassword }: any) { + return ( +
+ setPassword(e.target.value)} + /> +
+ ); +} diff --git a/frontend/src/components/LoginComponents/InputFields/Password/index.ts b/frontend/src/components/LoginComponents/InputFields/Password/index.ts new file mode 100644 index 000000000..b345e9aef --- /dev/null +++ b/frontend/src/components/LoginComponents/InputFields/Password/index.ts @@ -0,0 +1 @@ +export { default } from "./Password"; diff --git a/frontend/src/components/navbar/NavBar.tsx b/frontend/src/components/navbar/NavBar.tsx index 5bc2cc2a0..033cad8f8 100644 --- a/frontend/src/components/navbar/NavBar.tsx +++ b/frontend/src/components/navbar/NavBar.tsx @@ -24,8 +24,8 @@ function NavBar({ token }: any, { setToken }: any) {
Students - Users Projects + Users { diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index f8d679a65..eda6f56de 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -1,11 +1,15 @@ import { useState } from "react"; -import "./LoginPage.css"; -import { logIn } from "../../utils/api/login"; import { useNavigate } from "react-router-dom"; +import { logIn } from "../../utils/api/login"; + import SocialButtons from "../../components/LoginComponents/SocialButtons"; import WelcomeText from "../../components/LoginComponents/WelcomeText"; +import Email from "../../components/LoginComponents/InputFields/Email"; +import Password from "../../components/LoginComponents/InputFields/Password"; + import { VerticalDivider } from "./styles"; +import "./LoginPage.css"; function LoginPage({ setToken }: any) { const [email, setEmail] = useState(""); @@ -27,24 +31,8 @@ function LoginPage({ setToken }: any) {
-
- setEmail(e.target.value)} - /> -
-
- setPassword(e.target.value)} - /> -
+ +
Don't have an account? Ask an admin for an invite link
From de4a997545ae8a25c9b59f5cadef5ae90420e579 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 27 Mar 2022 19:50:14 +0200 Subject: [PATCH 149/536] just more styles --- .../InputFields/Email/Email.tsx | 4 ++- .../InputFields/Password/Password.tsx | 6 ++-- .../LoginComponents/InputFields/styles.ts | 12 +++++++ frontend/src/views/LoginPage/LoginPage.css | 33 +------------------ frontend/src/views/LoginPage/LoginPage.tsx | 14 ++++---- frontend/src/views/LoginPage/styles.ts | 23 +++++++++++++ 6 files changed, 50 insertions(+), 42 deletions(-) create mode 100644 frontend/src/components/LoginComponents/InputFields/styles.ts diff --git a/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx b/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx index 726f99432..a392e9d38 100644 --- a/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx +++ b/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx @@ -1,7 +1,9 @@ +import { Input } from "../styles"; + export default function Email({ email, setEmail }: any) { return (
- - -
+ -
+ -
+
@@ -41,9 +41,9 @@ function LoginPage({ setToken }: any) { Log In
-
-
-
+ + +
); } diff --git a/frontend/src/views/LoginPage/styles.ts b/frontend/src/views/LoginPage/styles.ts index 825153291..b21ea5e63 100644 --- a/frontend/src/views/LoginPage/styles.ts +++ b/frontend/src/views/LoginPage/styles.ts @@ -1,5 +1,28 @@ import styled from "styled-components"; +export const LoginPageContainer = styled.div` + height: fit-content; + text-align: center; + display: flex; + justify-content: center; + flex-direction: column; + margin: 4%; +`; + +export const LoginContainer = styled.div` + display: flex; + margin-right: auto; + margin-left: auto; +`; + +export const EmailLoginContainer = styled.div` + height: fit-content; + text-align: center; + display: flex; + justify-content: center; + flex-direction: column; +`; + export const VerticalDivider = styled.div` margin-right: 20px; padding-right: 20px; From ab906c43a8165082ff4a5bc2bcb3bbc7b479b19c Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 27 Mar 2022 23:26:13 +0200 Subject: [PATCH 150/536] more changes to login --- .../src/components/LoginComponents/index.ts | 2 ++ frontend/src/views/LoginPage/LoginPage.css | 12 ----------- frontend/src/views/LoginPage/LoginPage.tsx | 21 ++++++++++++------- frontend/src/views/LoginPage/styles.ts | 14 +++++++++++++ 4 files changed, 29 insertions(+), 20 deletions(-) create mode 100644 frontend/src/components/LoginComponents/index.ts diff --git a/frontend/src/components/LoginComponents/index.ts b/frontend/src/components/LoginComponents/index.ts new file mode 100644 index 000000000..4d11681fe --- /dev/null +++ b/frontend/src/components/LoginComponents/index.ts @@ -0,0 +1,2 @@ +export {default as WelcomeText} from "./WelcomeText" +export {default as SocialButtons}from "./SocialButtons" diff --git a/frontend/src/views/LoginPage/LoginPage.css b/frontend/src/views/LoginPage/LoginPage.css index 174bf42bd..34f2dbdef 100644 --- a/frontend/src/views/LoginPage/LoginPage.css +++ b/frontend/src/views/LoginPage/LoginPage.css @@ -22,15 +22,6 @@ border-radius: 10px; } -.login-button { - width: 120px; - height: 35px; - cursor: pointer; - background: var(--osoc_green); - color: white; - border: none; - border-radius: 5px; -} .socials-register { @@ -38,6 +29,3 @@ } -.no-account { - padding-bottom: 15px; -} diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index 3b5662290..f449cd44e 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -3,12 +3,19 @@ import { useNavigate } from "react-router-dom"; import { logIn } from "../../utils/api/login"; -import SocialButtons from "../../components/LoginComponents/SocialButtons"; -import WelcomeText from "../../components/LoginComponents/WelcomeText"; import Email from "../../components/LoginComponents/InputFields/Email"; import Password from "../../components/LoginComponents/InputFields/Password"; -import { LoginPageContainer, LoginContainer, EmailLoginContainer, VerticalDivider } from "./styles"; +import { WelcomeText, SocialButtons } from "../../components/LoginComponents"; + +import { + LoginPageContainer, + LoginContainer, + EmailLoginContainer, + VerticalDivider, + NoAccount, + LoginButton, +} from "./styles"; import "./LoginPage.css"; function LoginPage({ setToken }: any) { @@ -33,13 +40,11 @@ function LoginPage({ setToken }: any) { -
+ Don't have an account? Ask an admin for an invite link -
+
- + Log In
diff --git a/frontend/src/views/LoginPage/styles.ts b/frontend/src/views/LoginPage/styles.ts index b21ea5e63..fe3c9f764 100644 --- a/frontend/src/views/LoginPage/styles.ts +++ b/frontend/src/views/LoginPage/styles.ts @@ -28,3 +28,17 @@ export const VerticalDivider = styled.div` padding-right: 20px; border-right: 2px solid rgba(182, 182, 182, 0.603); `; + +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; +`; From 390c2e638d3b84712bb45a859b6fd2879acceb14 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Mon, 28 Mar 2022 16:05:15 +0200 Subject: [PATCH 151/536] Beter types and import updates --- .../LoginComponents/InputFields/Email/Email.tsx | 8 +++++++- .../LoginComponents/InputFields/Password/Password.tsx | 8 +++++++- frontend/src/components/LoginComponents/index.ts | 6 ++++-- frontend/src/utils/api/login.ts | 2 +- frontend/src/views/LoginPage/LoginPage.tsx | 5 +---- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx b/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx index a392e9d38..2a844b5b1 100644 --- a/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx +++ b/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx @@ -1,6 +1,12 @@ import { Input } from "../styles"; -export default function Email({ email, setEmail }: any) { +export default function Email({ + email, + setEmail, +}: { + email: string; + setEmail: (value: string) => void; +}) { return (
void; +}) { return (
Date: Mon, 28 Mar 2022 16:59:37 +0200 Subject: [PATCH 152/536] started on RegisterPage refactor --- .../ConfirmPassword/ConfirmPassword.tsx | 21 ++++++ .../InputFields/ConfirmPassword/index.ts | 1 + .../InputFields/Email/Email.tsx | 21 ++++++ .../InputFields/Email/index.ts | 1 + .../InputFields/Name/Name.tsx | 21 ++++++ .../InputFields/Name/index.ts | 1 + .../InputFields/Password/Password.tsx | 21 ++++++ .../InputFields/Password/index.ts | 1 + .../RegisterComponents/InputFields/styles.ts | 12 ++++ .../components/RegisterComponents/index.ts | 4 ++ frontend/src/utils/api/index.ts | 2 +- frontend/src/utils/api/register.ts | 29 ++++++++ .../api/{auth.ts => validateRegisterLink.ts} | 2 +- .../src/views/RegisterPage/RegisterPage.tsx | 70 +++++-------------- 14 files changed, 152 insertions(+), 55 deletions(-) create mode 100644 frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx create mode 100644 frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/index.ts create mode 100644 frontend/src/components/RegisterComponents/InputFields/Email/Email.tsx create mode 100644 frontend/src/components/RegisterComponents/InputFields/Email/index.ts create mode 100644 frontend/src/components/RegisterComponents/InputFields/Name/Name.tsx create mode 100644 frontend/src/components/RegisterComponents/InputFields/Name/index.ts create mode 100644 frontend/src/components/RegisterComponents/InputFields/Password/Password.tsx create mode 100644 frontend/src/components/RegisterComponents/InputFields/Password/index.ts create mode 100644 frontend/src/components/RegisterComponents/InputFields/styles.ts create mode 100644 frontend/src/components/RegisterComponents/index.ts create mode 100644 frontend/src/utils/api/register.ts rename frontend/src/utils/api/{auth.ts => validateRegisterLink.ts} (91%) diff --git a/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx b/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx new file mode 100644 index 000000000..1fb5befbd --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx @@ -0,0 +1,21 @@ +import { Input } from "../styles"; + +export default function ConfirmPassword({ + confirmPassword, + setConfirmPassword, +}: { + confirmPassword: string; + setConfirmPassword: (value: string) => void; +}) { + return ( +
+ setConfirmPassword(e.target.value)} + /> +
+ ); +} diff --git a/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/index.ts b/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/index.ts new file mode 100644 index 000000000..4b91ec8f8 --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/index.ts @@ -0,0 +1 @@ +export { default } from "./ConfirmPassword"; diff --git a/frontend/src/components/RegisterComponents/InputFields/Email/Email.tsx b/frontend/src/components/RegisterComponents/InputFields/Email/Email.tsx new file mode 100644 index 000000000..2a844b5b1 --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/Email/Email.tsx @@ -0,0 +1,21 @@ +import { Input } from "../styles"; + +export default function Email({ + email, + setEmail, +}: { + email: string; + setEmail: (value: string) => void; +}) { + return ( +
+ setEmail(e.target.value)} + /> +
+ ); +} diff --git a/frontend/src/components/RegisterComponents/InputFields/Email/index.ts b/frontend/src/components/RegisterComponents/InputFields/Email/index.ts new file mode 100644 index 000000000..f2681e1e2 --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/Email/index.ts @@ -0,0 +1 @@ +export { default } from "./Email"; diff --git a/frontend/src/components/RegisterComponents/InputFields/Name/Name.tsx b/frontend/src/components/RegisterComponents/InputFields/Name/Name.tsx new file mode 100644 index 000000000..35ceedee3 --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/Name/Name.tsx @@ -0,0 +1,21 @@ +import { Input } from "../styles"; + +export default function Name({ + name, + setName, +}: { + name: string; + setName: (value: string) => void; +}) { + return ( +
+ setName(e.target.value)} + /> +
+ ); +} diff --git a/frontend/src/components/RegisterComponents/InputFields/Name/index.ts b/frontend/src/components/RegisterComponents/InputFields/Name/index.ts new file mode 100644 index 000000000..4e90e41d5 --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/Name/index.ts @@ -0,0 +1 @@ +export { default } from "./Name"; diff --git a/frontend/src/components/RegisterComponents/InputFields/Password/Password.tsx b/frontend/src/components/RegisterComponents/InputFields/Password/Password.tsx new file mode 100644 index 000000000..799004c22 --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/Password/Password.tsx @@ -0,0 +1,21 @@ +import { Input } from "../styles"; + +export default function Password({ + password, + setPassword, +}: { + password: string; + setPassword: (value: string) => void; +}) { + return ( +
+ setPassword(e.target.value)} + /> +
+ ); +} diff --git a/frontend/src/components/RegisterComponents/InputFields/Password/index.ts b/frontend/src/components/RegisterComponents/InputFields/Password/index.ts new file mode 100644 index 000000000..b345e9aef --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/Password/index.ts @@ -0,0 +1 @@ +export { default } from "./Password"; diff --git a/frontend/src/components/RegisterComponents/InputFields/styles.ts b/frontend/src/components/RegisterComponents/InputFields/styles.ts new file mode 100644 index 000000000..a0bb3eeba --- /dev/null +++ b/frontend/src/components/RegisterComponents/InputFields/styles.ts @@ -0,0 +1,12 @@ +import styled from "styled-components"; + +export const Input = styled.input` + height: 40px; + width: 400px; + margin-top: 10px; + margin-bottom: 10px; + text-align: center; + font-size: 20px; + border-radius: 10px; + border-width: 0; +`; diff --git a/frontend/src/components/RegisterComponents/index.ts b/frontend/src/components/RegisterComponents/index.ts new file mode 100644 index 000000000..ef1a49d12 --- /dev/null +++ b/frontend/src/components/RegisterComponents/index.ts @@ -0,0 +1,4 @@ +export { default as Email } from "./InputFields/Email"; +export { default as Name } from "./InputFields/Name"; +export { default as Password } from "./InputFields/Password"; +export { default as ConfirmPassword } from "./InputFields/ConfirmPassword"; diff --git a/frontend/src/utils/api/index.ts b/frontend/src/utils/api/index.ts index c4dfe7af3..064c34216 100644 --- a/frontend/src/utils/api/index.ts +++ b/frontend/src/utils/api/index.ts @@ -1 +1 @@ -export { validateRegistrationUrl } from "./auth"; +export { validateRegistrationUrl } from "./validateRegisterLink"; diff --git a/frontend/src/utils/api/register.ts b/frontend/src/utils/api/register.ts new file mode 100644 index 000000000..0314a2a5e --- /dev/null +++ b/frontend/src/utils/api/register.ts @@ -0,0 +1,29 @@ +import axios from "axios"; +import { axiosInstance } from "./api"; + +interface RegisterFields { + email: string; + name: string; + uuid: string; + pw: string; +} + +export async function register( + edition: string, + email: string, + name: string, + uuid: string, + password: string +) { + const payload: RegisterFields = { email: email, name: name, uuid: uuid, pw: password }; + try { + await axiosInstance.post("/editions/" + edition + "/register/email", payload); + return true; + } catch (error) { + if (axios.isAxiosError(error)) { + return false; + } else { + throw error; + } + } +} diff --git a/frontend/src/utils/api/auth.ts b/frontend/src/utils/api/validateRegisterLink.ts similarity index 91% rename from frontend/src/utils/api/auth.ts rename to frontend/src/utils/api/validateRegisterLink.ts index f34d4480c..a5e1a6fba 100644 --- a/frontend/src/utils/api/auth.ts +++ b/frontend/src/utils/api/validateRegisterLink.ts @@ -5,7 +5,7 @@ import { axiosInstance } from "./api"; * Check if a registration url exists by sending a GET to it, * if it returns a 200 then we know the url is valid. */ -export async function validateRegistrationUrl(edition: string, uuid: string): Promise { +export async function validateRegistrationUrl(edition: string, uuid: string | undefined): Promise { try { await axiosInstance.get(`/editions/${edition}/invites/${uuid}`); return true; diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index dc0011ed9..54baedafe 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -1,18 +1,15 @@ import { useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { axiosInstance } from "../../utils/api/api"; -import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; +import { register } from "../../utils/api/register"; +import { validateRegistrationUrl } from "../../utils/api"; -interface RegisterFields { - email: string; - name: string; - uuid: string; - pw: string; -} +import { Email, Name, Password, ConfirmPassword } from "../../components/RegisterComponents"; + +import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; function RegisterPage() { - function register(uuid: string) { + function callRegister(uuid: string) { // Check if passwords are the same if (password !== confirmPassword) { alert("Passwords do not match"); @@ -26,11 +23,7 @@ function RegisterPage() { // TODO this has to change to get the edition the invite belongs to const edition = "1"; - const payload: RegisterFields = { email: email, name: name, uuid: uuid, pw: password }; - - axiosInstance - .post("/editions/" + edition + "/register/email", payload) - .then((response: any) => console.log(response)) + register(edition, email, name, uuid, password) .then(() => navigate("/pending")) .catch(function (error: any) { console.log(error); @@ -49,8 +42,8 @@ function RegisterPage() { const [validUuid, setUuid] = useState(false); - axiosInstance.get("/editions/" + 1 + "/invites/" + uuid).then(response => { - if (response.data.uuid === uuid) { + validateRegistrationUrl("1", uuid).then(response => { + if (response) { setUuid(true); } }); @@ -76,45 +69,16 @@ function RegisterPage() {

or

-
- setEmail(e.target.value)} - /> -
-
- setName(e.target.value)} - /> -
-
- setPassword(e.target.value)} - /> -
-
- setConfirmPassword(e.target.value)} - /> -
+ + + +
-
From 6d5200c087c457bc216da730207cc48b78709c59 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Tue, 29 Mar 2022 15:35:54 +0200 Subject: [PATCH 153/536] used await functions instead of then --- frontend/src/views/LoginPage/LoginPage.tsx | 11 ++-- .../src/views/RegisterPage/RegisterPage.tsx | 66 ++++++++++--------- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index aaca17aa4..8e9039fcb 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -20,11 +20,14 @@ function LoginPage({ setToken }: any) { const [password, setPassword] = useState(""); const navigate = useNavigate(); - function callLogIn() { - logIn({ setToken }, email, password).then(response => { + async function callLogIn() { + try { + const response = await logIn({ setToken }, email, password); if (response) navigate("/students"); - else alert("Login failed"); - }); + else alert("Something went wrong when login in"); + } catch (error) { + console.log(error); + } } return ( diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index 54baedafe..f5d447836 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -1,15 +1,35 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { register } from "../../utils/api/register"; import { validateRegistrationUrl } from "../../utils/api"; -import { Email, Name, Password, ConfirmPassword } from "../../components/RegisterComponents"; - -import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; +import { + Email, + Name, + Password, + ConfirmPassword, + SocialButtons, +} from "../../components/RegisterComponents"; function RegisterPage() { - function callRegister(uuid: string) { + const [validUuid, setUuid] = useState(false); + const params = useParams(); + const uuid = params.uuid; + + useEffect(() => { + async function validateUuid() { + const response = await validateRegistrationUrl("1", uuid); + if (response) { + setUuid(true); + } + } + if (!validUuid) { + validateUuid(); + } + }, [uuid, validUuid]); + + async function callRegister(uuid: string) { // Check if passwords are the same if (password !== confirmPassword) { alert("Passwords do not match"); @@ -23,11 +43,15 @@ function RegisterPage() { // TODO this has to change to get the edition the invite belongs to const edition = "1"; - register(edition, email, name, uuid, password) - .then(() => navigate("/pending")) - .catch(function (error: any) { - console.log(error); - }); + try { + const response = await register(edition, email, name, uuid, password); + if (response) { + navigate("/pending"); + } + } catch (error) { + console.log(error); + alert("Something went wrong when creating your account"); + } } const [email, setEmail] = useState(""); @@ -37,37 +61,17 @@ function RegisterPage() { const navigate = useNavigate(); - const params = useParams(); - const uuid = params.uuid; - - const [validUuid, setUuid] = useState(false); - - validateRegistrationUrl("1", uuid).then(response => { - if (response) { - setUuid(true); - } - }); - if (validUuid && uuid) { return (

Create an account

-
Sign up with your social media account or email address. Your unique link is not useable again ({uuid})
-
-
- -
- -
-
- +

or

-
From c294a9088ae2846af8267a5db8f61f01d4c86c96 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Tue, 29 Mar 2022 15:36:16 +0200 Subject: [PATCH 154/536] moved socialbuttons in different components --- .../SocialButtons/SocialButtons.tsx | 13 +++++++++++++ .../RegisterComponents/SocialButtons/index.ts | 1 + .../RegisterComponents/SocialButtons/styles.ts | 14 ++++++++++++++ .../src/components/RegisterComponents/index.ts | 1 + 4 files changed, 29 insertions(+) create mode 100644 frontend/src/components/RegisterComponents/SocialButtons/SocialButtons.tsx create mode 100644 frontend/src/components/RegisterComponents/SocialButtons/index.ts create mode 100644 frontend/src/components/RegisterComponents/SocialButtons/styles.ts diff --git a/frontend/src/components/RegisterComponents/SocialButtons/SocialButtons.tsx b/frontend/src/components/RegisterComponents/SocialButtons/SocialButtons.tsx new file mode 100644 index 000000000..6d19ea288 --- /dev/null +++ b/frontend/src/components/RegisterComponents/SocialButtons/SocialButtons.tsx @@ -0,0 +1,13 @@ +import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; +import { SocialsContainer, Socials } from "./styles"; + +export default function SocialButtons() { + return ( + + + + + + + ); +} diff --git a/frontend/src/components/RegisterComponents/SocialButtons/index.ts b/frontend/src/components/RegisterComponents/SocialButtons/index.ts new file mode 100644 index 000000000..1aa8d729f --- /dev/null +++ b/frontend/src/components/RegisterComponents/SocialButtons/index.ts @@ -0,0 +1 @@ +export { default } from "./SocialButtons"; diff --git a/frontend/src/components/RegisterComponents/SocialButtons/styles.ts b/frontend/src/components/RegisterComponents/SocialButtons/styles.ts new file mode 100644 index 000000000..4458c3f07 --- /dev/null +++ b/frontend/src/components/RegisterComponents/SocialButtons/styles.ts @@ -0,0 +1,14 @@ +import styled from "styled-components"; + +export const SocialsContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +export const Socials = styled.div` + justify-content: center; + display: flex; + min-width: 230px; + height: fit-content; +`; diff --git a/frontend/src/components/RegisterComponents/index.ts b/frontend/src/components/RegisterComponents/index.ts index ef1a49d12..69e19a874 100644 --- a/frontend/src/components/RegisterComponents/index.ts +++ b/frontend/src/components/RegisterComponents/index.ts @@ -2,3 +2,4 @@ export { default as Email } from "./InputFields/Email"; export { default as Name } from "./InputFields/Name"; export { default as Password } from "./InputFields/Password"; export { default as ConfirmPassword } from "./InputFields/ConfirmPassword"; +export { default as SocialButtons } from "./SocialButtons"; From e9b006d53f62cd3905fde7219e343a7dd1392430 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Tue, 29 Mar 2022 19:14:08 +0200 Subject: [PATCH 155/536] some more components for Registerpage Redirect when uuid is bad --- .../BadInviteLink/BadInviteLink.tsx | 5 +++ .../RegisterComponents/BadInviteLink/index.ts | 1 + .../BadInviteLink/styles.ts | 9 ++++ .../RegisterComponents/InfoText/InfoText.tsx | 13 ++++++ .../RegisterComponents/InfoText/index.ts | 1 + .../RegisterComponents/InfoText/styles.ts | 10 +++++ .../components/RegisterComponents/index.ts | 2 + frontend/src/views/LoginPage/LoginPage.tsx | 1 - .../src/views/RegisterPage/RegisterPage.tsx | 44 +++++++++---------- .../LoginPage.css => RegisterPage/styles.ts} | 22 ++++------ 10 files changed, 72 insertions(+), 36 deletions(-) create mode 100644 frontend/src/components/RegisterComponents/BadInviteLink/BadInviteLink.tsx create mode 100644 frontend/src/components/RegisterComponents/BadInviteLink/index.ts create mode 100644 frontend/src/components/RegisterComponents/BadInviteLink/styles.ts create mode 100644 frontend/src/components/RegisterComponents/InfoText/InfoText.tsx create mode 100644 frontend/src/components/RegisterComponents/InfoText/index.ts create mode 100644 frontend/src/components/RegisterComponents/InfoText/styles.ts rename frontend/src/views/{LoginPage/LoginPage.css => RegisterPage/styles.ts} (61%) diff --git a/frontend/src/components/RegisterComponents/BadInviteLink/BadInviteLink.tsx b/frontend/src/components/RegisterComponents/BadInviteLink/BadInviteLink.tsx new file mode 100644 index 000000000..db8a330c2 --- /dev/null +++ b/frontend/src/components/RegisterComponents/BadInviteLink/BadInviteLink.tsx @@ -0,0 +1,5 @@ +import { BadInvite } from "./styles"; + +export default function BadInviteLink() { + return Not a valid register url. You will be redirected.; +} diff --git a/frontend/src/components/RegisterComponents/BadInviteLink/index.ts b/frontend/src/components/RegisterComponents/BadInviteLink/index.ts new file mode 100644 index 000000000..c32065aab --- /dev/null +++ b/frontend/src/components/RegisterComponents/BadInviteLink/index.ts @@ -0,0 +1 @@ +export { default } from "./BadInviteLink"; diff --git a/frontend/src/components/RegisterComponents/BadInviteLink/styles.ts b/frontend/src/components/RegisterComponents/BadInviteLink/styles.ts new file mode 100644 index 000000000..9820ef7a2 --- /dev/null +++ b/frontend/src/components/RegisterComponents/BadInviteLink/styles.ts @@ -0,0 +1,9 @@ +import styled from "styled-components"; + +export const BadInvite = styled.div` + margin: auto; + margin-top: 10%; + max-width: 50%; + text-align: center; + font-size: 20px; +`; diff --git a/frontend/src/components/RegisterComponents/InfoText/InfoText.tsx b/frontend/src/components/RegisterComponents/InfoText/InfoText.tsx new file mode 100644 index 000000000..ced927301 --- /dev/null +++ b/frontend/src/components/RegisterComponents/InfoText/InfoText.tsx @@ -0,0 +1,13 @@ +import { TitleText, Info } from "./styles"; + +export default function InfoText() { + return ( +
+ Create an account + + Sign up with your social media account or email address. Your unique link is not + useable again. + +
+ ); +} diff --git a/frontend/src/components/RegisterComponents/InfoText/index.ts b/frontend/src/components/RegisterComponents/InfoText/index.ts new file mode 100644 index 000000000..ef56695bb --- /dev/null +++ b/frontend/src/components/RegisterComponents/InfoText/index.ts @@ -0,0 +1 @@ +export { default } from "./InfoText"; diff --git a/frontend/src/components/RegisterComponents/InfoText/styles.ts b/frontend/src/components/RegisterComponents/InfoText/styles.ts new file mode 100644 index 000000000..319fc8495 --- /dev/null +++ b/frontend/src/components/RegisterComponents/InfoText/styles.ts @@ -0,0 +1,10 @@ +import styled from "styled-components"; + +export const TitleText = styled.h1` + margin-bottom: 10px; +`; + +export const Info = styled.div` + color: grey; + margin-bottom: 10px; +`; diff --git a/frontend/src/components/RegisterComponents/index.ts b/frontend/src/components/RegisterComponents/index.ts index 69e19a874..f60693215 100644 --- a/frontend/src/components/RegisterComponents/index.ts +++ b/frontend/src/components/RegisterComponents/index.ts @@ -3,3 +3,5 @@ export { default as Name } from "./InputFields/Name"; export { default as Password } from "./InputFields/Password"; 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"; diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index 8e9039fcb..ef19fd0bc 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -13,7 +13,6 @@ import { NoAccount, LoginButton, } from "./styles"; -import "./LoginPage.css"; function LoginPage({ setToken }: any) { const [email, setEmail] = useState(""); diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index f5d447836..2fcc1662b 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -10,8 +10,12 @@ import { Password, ConfirmPassword, SocialButtons, + InfoText, + BadInviteLink, } from "../../components/RegisterComponents"; +import { RegisterFormContainer, Or, RegisterButton } from "./styles"; + function RegisterPage() { const [validUuid, setUuid] = useState(false); const params = useParams(); @@ -22,12 +26,16 @@ function RegisterPage() { const response = await validateRegistrationUrl("1", uuid); if (response) { setUuid(true); + } else { + setTimeout(() => { + navigate("/*"); + }, 5000); } } if (!validUuid) { validateUuid(); } - }, [uuid, validUuid]); + }); async function callRegister(uuid: string) { // Check if passwords are the same @@ -64,32 +72,24 @@ function RegisterPage() { if (validUuid && uuid) { return (
-
-

Create an account

-
- Sign up with your social media account or email address. Your unique link is - not useable again ({uuid}) -
+ + -

or

-
- - - - -
+ or + + + +
- + callRegister(uuid)}>Register
-
+
); - } else return
Not a valid register url
; + } else return ; } export default RegisterPage; diff --git a/frontend/src/views/LoginPage/LoginPage.css b/frontend/src/views/RegisterPage/styles.ts similarity index 61% rename from frontend/src/views/LoginPage/LoginPage.css rename to frontend/src/views/RegisterPage/styles.ts index 34f2dbdef..3ae125b1f 100644 --- a/frontend/src/views/LoginPage/LoginPage.css +++ b/frontend/src/views/RegisterPage/styles.ts @@ -1,16 +1,19 @@ +import styled from "styled-components"; - -.register-form-content-container { +export const RegisterFormContainer = styled.div` height: fit-content; text-align: center; display: flex; justify-content: center; flex-direction: column; -} - + margin: 4%; +`; +export const Or = styled.h2` + margin: 10px; +`; -.register-button { +export const RegisterButton = styled.button` height: 40px; width: 400px; margin-top: 10px; @@ -20,12 +23,5 @@ color: white; border: none; border-radius: 10px; -} - - - -.socials-register { - display: flex; -} - +`; From 8c8ef370610d66ec1ce2e1b9d7f44b8b315139c1 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 30 Mar 2022 12:15:00 +0200 Subject: [PATCH 156/536] removed some css in favour of styled components --- frontend/src/views/ErrorPage/ErrorPage.tsx | 6 +++--- frontend/src/views/ErrorPage/{ErrorPage.css => styles.ts} | 6 ++++-- frontend/src/views/RegisterPage/RegisterPage.css | 0 3 files changed, 7 insertions(+), 5 deletions(-) rename frontend/src/views/ErrorPage/{ErrorPage.css => styles.ts} (50%) delete mode 100644 frontend/src/views/RegisterPage/RegisterPage.css diff --git a/frontend/src/views/ErrorPage/ErrorPage.tsx b/frontend/src/views/ErrorPage/ErrorPage.tsx index e82b5ad8a..5b2679219 100644 --- a/frontend/src/views/ErrorPage/ErrorPage.tsx +++ b/frontend/src/views/ErrorPage/ErrorPage.tsx @@ -1,11 +1,11 @@ import React from "react"; -import "./ErrorPage.css"; +import { ErrorMessage } from "./styles"; function ErrorPage() { return ( -

+ Oops! This is awkward... You are looking for something that doesn't actually exists. -

+ ); } diff --git a/frontend/src/views/ErrorPage/ErrorPage.css b/frontend/src/views/ErrorPage/styles.ts similarity index 50% rename from frontend/src/views/ErrorPage/ErrorPage.css rename to frontend/src/views/ErrorPage/styles.ts index 3dadd669f..23e2ce164 100644 --- a/frontend/src/views/ErrorPage/ErrorPage.css +++ b/frontend/src/views/ErrorPage/styles.ts @@ -1,6 +1,8 @@ -.error { +import styled from "styled-components"; + +export const ErrorMessage = styled.h1` margin: auto; margin-top: 10%; max-width: 50%; text-align: center; -} +`; diff --git a/frontend/src/views/RegisterPage/RegisterPage.css b/frontend/src/views/RegisterPage/RegisterPage.css deleted file mode 100644 index e69de29bb..000000000 From eff19872fd0bc99e0fd1c5e84bd1446f953e9cfc Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 30 Mar 2022 14:00:16 +0200 Subject: [PATCH 157/536] relocated app.css file removed redirect when invalid uuid --- frontend/src/{css-files => }/App.css | 0 frontend/src/App.tsx | 2 +- frontend/src/components/navbar/NavBarElements.tsx | 2 +- frontend/src/views/RegisterPage/RegisterPage.tsx | 4 ---- 4 files changed, 2 insertions(+), 6 deletions(-) rename frontend/src/{css-files => }/App.css (100%) diff --git a/frontend/src/css-files/App.css b/frontend/src/App.css similarity index 100% rename from frontend/src/css-files/App.css rename to frontend/src/App.css diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 278ed30e4..05c360e6f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import "bootstrap/dist/css/bootstrap.min.css"; -import "./css-files/App.css"; +import "./App.css"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import NavBar from "./components/navbar"; import LoginPage from "./views/LoginPage"; diff --git a/frontend/src/components/navbar/NavBarElements.tsx b/frontend/src/components/navbar/NavBarElements.tsx index 1f76511f6..0712b05de 100644 --- a/frontend/src/components/navbar/NavBarElements.tsx +++ b/frontend/src/components/navbar/NavBarElements.tsx @@ -2,7 +2,7 @@ import styled from "styled-components"; import { NavLink as Link } from "react-router-dom"; import { FaBars } from "react-icons/fa"; -import "../../css-files/App.css"; +import "../../App.css"; export const Nav = styled.nav` background: var(--osoc_blue); diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index 2fcc1662b..b6fb8e637 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -26,10 +26,6 @@ function RegisterPage() { const response = await validateRegistrationUrl("1", uuid); if (response) { setUuid(true); - } else { - setTimeout(() => { - navigate("/*"); - }, 5000); } } if (!validUuid) { From 11bf413462985b8d2fd8961aad00438cd3c1b52d Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 30 Mar 2022 14:08:41 +0200 Subject: [PATCH 158/536] prettier --- frontend/src/utils/api/validateRegisterLink.ts | 5 ++++- frontend/src/views/RegisterPage/styles.ts | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/utils/api/validateRegisterLink.ts b/frontend/src/utils/api/validateRegisterLink.ts index a5e1a6fba..7f4be598c 100644 --- a/frontend/src/utils/api/validateRegisterLink.ts +++ b/frontend/src/utils/api/validateRegisterLink.ts @@ -5,7 +5,10 @@ import { axiosInstance } from "./api"; * Check if a registration url exists by sending a GET to it, * if it returns a 200 then we know the url is valid. */ -export async function validateRegistrationUrl(edition: string, uuid: string | undefined): Promise { +export async function validateRegistrationUrl( + edition: string, + uuid: string | undefined +): Promise { try { await axiosInstance.get(`/editions/${edition}/invites/${uuid}`); return true; diff --git a/frontend/src/views/RegisterPage/styles.ts b/frontend/src/views/RegisterPage/styles.ts index 3ae125b1f..01f97fd76 100644 --- a/frontend/src/views/RegisterPage/styles.ts +++ b/frontend/src/views/RegisterPage/styles.ts @@ -24,4 +24,3 @@ export const RegisterButton = styled.button` border: none; border-radius: 10px; `; - From 557169fbe52ece95ec7793eed67e1b08898f4afa Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 30 Mar 2022 14:58:40 +0200 Subject: [PATCH 159/536] More styled components for pending page --- .../src/views/PendingPage/PendingPage.css | 34 ------------------- .../src/views/PendingPage/PendingPage.tsx | 23 ++++++++----- frontend/src/views/PendingPage/styles.ts | 25 ++++++++++++++ 3 files changed, 40 insertions(+), 42 deletions(-) create mode 100644 frontend/src/views/PendingPage/styles.ts diff --git a/frontend/src/views/PendingPage/PendingPage.css b/frontend/src/views/PendingPage/PendingPage.css index b2a56fe3d..387c6e4bd 100644 --- a/frontend/src/views/PendingPage/PendingPage.css +++ b/frontend/src/views/PendingPage/PendingPage.css @@ -1,37 +1,3 @@ -/* -PendingPage - */ - -.pending-page-content-container { - height: fit-content; - position: absolute; - text-align: center; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} - -.pending-page-content-container span { - font-size: 40px; - display: block; - position: relative; -} - -.pending-page-content { - max-width: 1200px; - text-align: center; - height: fit-content; -} - -.pending-text-container { - width: fit-content; - display: inline-block; -} - -.pending-text-content { - display: flex; -} - .pending-dot-1 { animation: visible-switch-1 2s infinite linear; } diff --git a/frontend/src/views/PendingPage/PendingPage.tsx b/frontend/src/views/PendingPage/PendingPage.tsx index 313167da5..b9e937f43 100644 --- a/frontend/src/views/PendingPage/PendingPage.tsx +++ b/frontend/src/views/PendingPage/PendingPage.tsx @@ -1,24 +1,31 @@ import React from "react"; import "./PendingPage.css"; +import { + PendingPageContainer, + PendingContainer, + PendingTextContainer, + PendingText, +} from "./styles"; + function PendingPage() { return (
-
-
-
-
+ + + +

Your request is pending

.

.

.

-
-
+ +

Please wait until an admin approves your request!

-
-
+ +
); } diff --git a/frontend/src/views/PendingPage/styles.ts b/frontend/src/views/PendingPage/styles.ts new file mode 100644 index 000000000..bd287800a --- /dev/null +++ b/frontend/src/views/PendingPage/styles.ts @@ -0,0 +1,25 @@ +import styled from "styled-components"; + +export const PendingPageContainer = styled.div` + height: fit-content; + position: absolute; + text-align: center; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +`; + +export const PendingContainer = styled.div` + max-width: 1200px; + text-align: center; + height: fit-content; +`; + +export const PendingTextContainer = styled.div` + width: fit-content; + display: inline-block; +`; + +export const PendingText = styled.div` + display: flex; +`; From a9887fe5e99e90894c1ddb6e9812348bffbf0307 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 30 Mar 2022 14:59:53 +0200 Subject: [PATCH 160/536] Removed inline style for project page --- frontend/src/views/ProjectsPage/ProjectsPage.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index ad2716db1..d6469892f 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -2,18 +2,7 @@ import React from "react"; import "./ProjectsPage.css"; function ProjectPage() { - return ( -
- This is the projects page -
- ); + return
This is the projects page
; } export default ProjectPage; From a0d13a36aad1521b992a4c3f67916c190a5e9734 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 30 Mar 2022 15:01:36 +0200 Subject: [PATCH 161/536] typo in errorpage --- frontend/src/views/ErrorPage/ErrorPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/ErrorPage/ErrorPage.tsx b/frontend/src/views/ErrorPage/ErrorPage.tsx index 5b2679219..74be07bdf 100644 --- a/frontend/src/views/ErrorPage/ErrorPage.tsx +++ b/frontend/src/views/ErrorPage/ErrorPage.tsx @@ -4,7 +4,7 @@ import { ErrorMessage } from "./styles"; function ErrorPage() { return ( - Oops! This is awkward... You are looking for something that doesn't actually exists. + Oops! This is awkward... You are looking for something that doesn't actually exist. ); } From d67f35398c798d51832e9f510f789b87d1a8b5f4 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Thu, 31 Mar 2022 11:20:18 +0200 Subject: [PATCH 162/536] some style changes --- .../RegisterComponents/BadInviteLink/BadInviteLink.tsx | 2 +- .../src/components/RegisterComponents/BadInviteLink/styles.ts | 3 ++- frontend/src/views/PendingPage/styles.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/RegisterComponents/BadInviteLink/BadInviteLink.tsx b/frontend/src/components/RegisterComponents/BadInviteLink/BadInviteLink.tsx index db8a330c2..def284545 100644 --- a/frontend/src/components/RegisterComponents/BadInviteLink/BadInviteLink.tsx +++ b/frontend/src/components/RegisterComponents/BadInviteLink/BadInviteLink.tsx @@ -1,5 +1,5 @@ import { BadInvite } from "./styles"; export default function BadInviteLink() { - return Not a valid register url. You will be redirected.; + return Not a valid register url.; } diff --git a/frontend/src/components/RegisterComponents/BadInviteLink/styles.ts b/frontend/src/components/RegisterComponents/BadInviteLink/styles.ts index 9820ef7a2..88d76fc1a 100644 --- a/frontend/src/components/RegisterComponents/BadInviteLink/styles.ts +++ b/frontend/src/components/RegisterComponents/BadInviteLink/styles.ts @@ -5,5 +5,6 @@ export const BadInvite = styled.div` margin-top: 10%; max-width: 50%; text-align: center; - font-size: 20px; + font-size: 40px; + font-weight: 600; `; diff --git a/frontend/src/views/PendingPage/styles.ts b/frontend/src/views/PendingPage/styles.ts index bd287800a..02b9472ec 100644 --- a/frontend/src/views/PendingPage/styles.ts +++ b/frontend/src/views/PendingPage/styles.ts @@ -4,7 +4,7 @@ export const PendingPageContainer = styled.div` height: fit-content; position: absolute; text-align: center; - top: 50%; + top: 40%; left: 50%; transform: translate(-50%, -50%); `; From 42189b25abd89f0bd97b3999ad13d082b0bee54f Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Thu, 31 Mar 2022 14:30:41 +0200 Subject: [PATCH 163/536] submit input fields using enter key --- .../LoginComponents/InputFields/Password/Password.tsx | 7 +++++++ .../InputFields/ConfirmPassword/ConfirmPassword.tsx | 11 +++++++++-- frontend/src/views/LoginPage/LoginPage.tsx | 2 +- frontend/src/views/RegisterPage/RegisterPage.tsx | 1 + 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx b/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx index 799004c22..d94215140 100644 --- a/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx +++ b/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx @@ -3,9 +3,11 @@ import { Input } from "../styles"; export default function Password({ password, setPassword, + callLogIn }: { password: string; setPassword: (value: string) => void; + callLogIn: () => void; }) { return (
@@ -15,6 +17,11 @@ export default function Password({ placeholder="Password" value={password} onChange={e => setPassword(e.target.value)} + onKeyPress={e => { + if (e.key === "Enter") { + callLogIn() + } + }} />
); diff --git a/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx b/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx index 1fb5befbd..40383be78 100644 --- a/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx +++ b/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx @@ -3,18 +3,25 @@ import { Input } from "../styles"; export default function ConfirmPassword({ confirmPassword, setConfirmPassword, + callRegister, }: { confirmPassword: string; setConfirmPassword: (value: string) => void; + callRegister: () => void; }) { return (
setConfirmPassword(e.target.value)} + onKeyPress={e => { + if (e.key === "Enter") { + callRegister(); + } + }} />
); diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index ef19fd0bc..514ad2973 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -38,7 +38,7 @@ function LoginPage({ setToken }: any) { - + Don't have an account? Ask an admin for an invite link diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index b6fb8e637..34a81945c 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -78,6 +78,7 @@ function RegisterPage() { callRegister(uuid)} />
callRegister(uuid)}>Register From 553f07ffae8a9c59ced5a5e584cd2c207253ff6e Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Thu, 31 Mar 2022 14:56:23 +0200 Subject: [PATCH 164/536] put uuid in if statement to prevent undefined --- frontend/src/utils/api/validateRegisterLink.ts | 5 +---- frontend/src/views/RegisterPage/RegisterPage.tsx | 8 +++++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/src/utils/api/validateRegisterLink.ts b/frontend/src/utils/api/validateRegisterLink.ts index 7f4be598c..f34d4480c 100644 --- a/frontend/src/utils/api/validateRegisterLink.ts +++ b/frontend/src/utils/api/validateRegisterLink.ts @@ -5,10 +5,7 @@ import { axiosInstance } from "./api"; * Check if a registration url exists by sending a GET to it, * if it returns a 200 then we know the url is valid. */ -export async function validateRegistrationUrl( - edition: string, - uuid: string | undefined -): Promise { +export async function validateRegistrationUrl(edition: string, uuid: string): Promise { try { await axiosInstance.get(`/editions/${edition}/invites/${uuid}`); return true; diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index b6fb8e637..6190e7743 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -23,9 +23,11 @@ function RegisterPage() { useEffect(() => { async function validateUuid() { - const response = await validateRegistrationUrl("1", uuid); - if (response) { - setUuid(true); + if (uuid) { + const response = await validateRegistrationUrl("1", uuid); + if (response) { + setUuid(true); + } } } if (!validUuid) { From 73155252e99bd989a0d68b1b3c52128d58dfaf03 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Thu, 31 Mar 2022 15:03:07 +0200 Subject: [PATCH 165/536] prettier --- .../LoginComponents/InputFields/Password/Password.tsx | 4 ++-- frontend/src/views/LoginPage/LoginPage.tsx | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx b/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx index d94215140..ff044402f 100644 --- a/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx +++ b/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx @@ -3,7 +3,7 @@ import { Input } from "../styles"; export default function Password({ password, setPassword, - callLogIn + callLogIn, }: { password: string; setPassword: (value: string) => void; @@ -19,7 +19,7 @@ export default function Password({ onChange={e => setPassword(e.target.value)} onKeyPress={e => { if (e.key === "Enter") { - callLogIn() + callLogIn(); } }} /> diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index 514ad2973..435460534 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -38,7 +38,11 @@ function LoginPage({ setToken }: any) { - + Don't have an account? Ask an admin for an invite link From 5832bf86052752474862f8b35c2af0741c6bd765 Mon Sep 17 00:00:00 2001 From: beguille Date: Fri, 1 Apr 2022 13:00:13 +0200 Subject: [PATCH 166/536] changed conflictstudent structure --- backend/src/app/logic/projects.py | 12 ++++++---- backend/src/app/schemas/projects.py | 23 +++++++------------ .../test_students/test_students.py | 3 +-- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index fbaadf8a1..a8223033b 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -1,6 +1,7 @@ from sqlalchemy.orm import Session -from src.app.schemas.projects import ProjectList, Project, ConflictStudentList, InputProject, Student, ConflictStudent +from src.app.schemas.projects import ProjectList, Project, ConflictStudentList, InputProject, Student, ConflictStudent, \ + ConflictProject from src.database.crud.projects import db_get_all_projects, db_add_project, db_delete_project, \ db_patch_project, db_get_conflict_students from src.database.models import Edition @@ -44,11 +45,12 @@ def logic_get_conflicts(db: Session, edition: Edition) -> ConflictStudentList: for student, projects in conflicts: projects_model = [] for project in projects: - project_model = (project.project_id, project.name) - print(type(project.project_id)) + project_model = ConflictProject(project_id=project.project_id, name=project.name) projects_model.append(project_model) - conflicts_model.append(ConflictStudent(student_id=student.student_id, student_first_name=student.first_name, - student_last_name=student.last_name, projects=projects_model)) + conflicts_model.append(ConflictStudent(student=Student(student_id=student.student_id, + first_name=student.first_name, + last_name=student.last_name), + projects=projects_model)) return ConflictStudentList(conflict_students=conflicts_model, edition_name=edition.name) diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index bac82b756..55905503d 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -67,21 +67,16 @@ class Config: class Student(CamelCaseModel): - """Represents a Student from the database to use in ConflictStudent""" + """Represents a Student to use in ConflictStudent""" student_id: int first_name: str last_name: str - preferred_name: str - email_address: str - phone_number: str | None - alumni: bool - decision: DecisionEnum - wants_to_be_student_coach: bool - edition_name: str - class Config: - """Set to ORM mode""" - orm_mode = True + +class ConflictProject(CamelCaseModel): + """A project to be used in ConflictStudent""" + project_id: int + name: str class ProjectList(CamelCaseModel): @@ -91,10 +86,8 @@ class ProjectList(CamelCaseModel): class ConflictStudent(CamelCaseModel): """A student together with the projects they are causing a conflict for""" - student_first_name: str - student_last_name: str - student_id: int - projects: list[tuple[int, str]] + student: Student + projects: list[ConflictProject] class ConflictStudentList(CamelCaseModel): 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 aa738c51e..09329142b 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 @@ -279,8 +279,7 @@ def test_get_conflicts(database_with_data: Session, test_client: TestClient): """test get the conflicts""" response = test_client.get("/editions/ed2022/projects/conflicts") json = response.json() - print(json) assert len(json['conflictStudents']) == 1 - assert json['conflictStudents'][0]['studentId'] == 1 + assert json['conflictStudents'][0]['student']['studentId'] == 1 assert len(json['conflictStudents'][0]['projects']) == 2 assert json['editionName'] == "ed2022" From 4ccb369f0aaab518324b54afca121e54adf5ce23 Mon Sep 17 00:00:00 2001 From: beguille Date: Fri, 1 Apr 2022 13:06:20 +0200 Subject: [PATCH 167/536] re-added TODOs --- backend/src/app/logic/security.py | 4 ++-- backend/src/app/routers/editions/editions.py | 2 +- backend/src/app/schemas/projects.py | 2 +- backend/src/app/utils/dependencies.py | 2 +- backend/src/database/crud/projects.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/app/logic/security.py b/backend/src/app/logic/security.py index 20b07c27f..4564d9ac9 100644 --- a/backend/src/app/logic/security.py +++ b/backend/src/app/logic/security.py @@ -39,14 +39,14 @@ def get_password_hash(password: str) -> str: return pwd_context.hash(password) -# TO DO remove this when the users crud has been implemented +# TODO remove this when the users crud has been implemented def get_user_by_email(db: Session, email: str) -> models.User: """Find a user by their email address""" auth_email = db.query(models.AuthEmail).where(models.AuthEmail.email == email).one() return db.query(models.User).where(models.User.user_id == auth_email.user_id).one() -# TO DO remove this when the users crud has been implemented +# TODO remove this when the users crud has been implemented def get_user_by_id(db: Session, user_id: int) -> models.User: """Find a user by their id""" return db.query(models.User).where(models.User.user_id == user_id).one() diff --git a/backend/src/app/routers/editions/editions.py b/backend/src/app/routers/editions/editions.py index 7d64dc883..178fa8a9e 100644 --- a/backend/src/app/routers/editions/editions.py +++ b/backend/src/app/routers/editions/editions.py @@ -42,7 +42,7 @@ async def get_editions(db: Session = Depends(get_session)): Returns: EditionList: an object with a list of all the editions. """ - # TO DO only return editions the user can see + # TODO only return editions the user can see return logic_editions.get_editions(db) diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index eb5cd6937..727d9460c 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -109,7 +109,7 @@ class InputProject(BaseModel): coaches: list[int] -# TO DO: change drafter_id to current user with authentication +# TODO: change drafter_id to current user with authentication class InputStudentRole(BaseModel): """Used for creating/patching a student role (temporary until authentication is implemented)""" skill_id: int diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index 73b89c832..4c730328d 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -15,7 +15,7 @@ from src.database.models import Edition, InviteLink, User, Project -# TO DO: Might be nice to use a more descriptive year number here than primary id. +# TODO: Might be nice to use a more descriptive year number here than primary id. def get_edition(edition_id: int, database: Session = Depends(get_session)) -> Edition: """Get an edition from the database, given the id in the path""" return get_edition_by_id(database, edition_id) diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index 551835c92..93a5398f0 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -40,7 +40,7 @@ def db_get_project(db: Session, project_id: int) -> Project: def db_delete_project(db: Session, project_id: int): """Delete a specific project from the database""" - # Maybe make the relationship between project and project_role cascade on delete? + # TODO: Maybe make the relationship between project and project_role cascade on delete? # so this code is handled by the database proj_roles = db.query(ProjectRole).where(ProjectRole.project_id == project_id).all() for proj_role in proj_roles: From 81c1d079e83cce41dd85e266055de0c5b40c22d1 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 1 Apr 2022 14:19:49 +0200 Subject: [PATCH 168/536] got errors since last night, so reinstall everything --- backend/src/app/logic/students.py | 9 ++++++--- .../src/app/routers/editions/students/students.py | 10 ++++++---- backend/src/app/schemas/students.py | 13 +++++++++++++ backend/src/database/crud/students.py | 2 +- .../test_editions/test_students/test_students.py | 6 ++++++ 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index 7ad484980..2c91bacf4 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -3,7 +3,8 @@ from src.app.schemas.students import NewDecision from src.database.crud.students import set_definitive_decision_on_student, delete_student, get_students from src.database.models import Edition, Student -from src.app.schemas.students import ReturnStudentList, ReturnStudent +from src.app.schemas.students import ReturnStudentList, ReturnStudent, CommonQueryParams + def definitive_decision_on_student(db: Session, student: Student, decision: NewDecision) -> None: """Set a definitive decion on a student""" @@ -15,9 +16,11 @@ def remove_student(db: Session, student: Student) -> None: delete_student(db, student) -def get_students_search(db: Session, edition: Edition) -> ReturnStudentList: +def get_students_search(db: Session, edition: Edition, commons: CommonQueryParams) -> ReturnStudentList: """return all students""" - students = get_students(db, edition) + # TODO: skill_ids to skill's + students = get_students(db, edition, first_name=commons.first_name, + last_name=commons.last_name, alumni=commons.alumni, student_coach=commons.student_coach) return ReturnStudentList(students=students) diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index af769b19d..d91b7e42c 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -5,7 +5,7 @@ from src.app.routers.tags import Tags from src.app.utils.dependencies import get_student, get_edition, require_admin, require_authorization from src.app.logic.students import definitive_decision_on_student, remove_student, get_student_return, get_students_search -from src.app.schemas.students import NewDecision +from src.app.schemas.students import NewDecision, CommonQueryParams, ReturnStudent, ReturnStudentList from src.database.database import get_session from src.database.models import Student, Edition from .suggestions import students_suggestions_router @@ -16,11 +16,13 @@ @students_router.get("/", dependencies=[Depends(require_authorization)]) -async def get_students(db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): +async def get_students(db: Session = Depends(get_session), + commons: CommonQueryParams = Depends(CommonQueryParams), + edition: Edition = Depends(get_edition)) -> ReturnStudentList: """ Get a list of all students. """ - get_students_search(db, edition) + return get_students_search(db, edition, commons) @students_router.post("/emails") @@ -39,7 +41,7 @@ async def delete_student(student: Student = Depends(get_student), db: Session = @students_router.get("/{student_id}", dependencies=[Depends(require_authorization)]) -async def get_student_by_id(edition: Edition = Depends(get_edition), student: Student = Depends(get_student)): +async def get_student_by_id(edition: Edition = Depends(get_edition), student: Student = Depends(get_student)) -> ReturnStudent: """ Get information about a specific student. """ diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index e26803a36..2bd5e56d9 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -40,3 +40,16 @@ class ReturnStudentList(CamelCaseModel): Model to return a list of students """ students: list[Student] + + +class CommonQueryParams: + """search query paramaters""" + + def __init__(self, first_name: str = "", last_name: str = "", alumni: bool = False, + student_coach: bool = False, skill_ids: list[int] = None) -> None: + """init""" + self.first_name = first_name + self.last_name = last_name + self.alumni = alumni + self.student_coach = student_coach + self.skill_ids = skill_ids diff --git a/backend/src/database/crud/students.py b/backend/src/database/crud/students.py index 9e800e22e..b4475d481 100644 --- a/backend/src/database/crud/students.py +++ b/backend/src/database/crud/students.py @@ -22,7 +22,7 @@ def delete_student(db: Session, student: Student) -> None: db.commit() -def get_students(db: Session, edition: Edition ,first_name: str = "", last_name: str = "", alumni: bool = False, student_coach: bool = False, skills: list[Skill] = None) -> list[Student]: +def get_students(db: Session, edition: Edition, first_name: str = "", last_name: str = "", alumni: bool = False, student_coach: bool = False, skills: list[Skill] = None) -> list[Student]: """Get students""" query = db.query(Student)\ .where(Student.edition == edition)\ 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 fb2f91c7b..a11333fa6 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 @@ -211,3 +211,9 @@ def test_get_all_students(database_with_data: Session, test_client: TestClient, response = test_client.get("/editions/1/students/", headers={"Authorization": auth_admin}) assert response.status_code == status.HTTP_200_OK + +def test_get_first_name_students(database_with_data: Session, test_client: TestClient, auth_admin: str): + """tests""" + response = test_client.get("/editions/1/students/?first_name='jos'", + headers={"Authorization": auth_admin}) + assert response.status_code == status.HTTP_200_OK From 9fff1c398e0a14f38ab2345bdaf8e354dcac3e36 Mon Sep 17 00:00:00 2001 From: beguille Date: Fri, 1 Apr 2022 15:06:03 +0200 Subject: [PATCH 169/536] closes #110, made confirm project_role route, added extra checks for adding project_role --- backend/src/app/exceptions/handlers.py | 15 +++ backend/src/app/exceptions/projects.py | 10 ++ backend/src/app/logic/projects_students.py | 46 ++++++++- .../projects/students/projects_students.py | 17 +++- .../src/database/crud/projects_students.py | 10 +- .../test_students/test_students.py | 95 +++++++++++++++---- 6 files changed, 166 insertions(+), 27 deletions(-) create mode 100644 backend/src/app/exceptions/projects.py diff --git a/backend/src/app/exceptions/handlers.py b/backend/src/app/exceptions/handlers.py index 3faf655db..1f7c10327 100644 --- a/backend/src/app/exceptions/handlers.py +++ b/backend/src/app/exceptions/handlers.py @@ -7,6 +7,7 @@ from .authentication import ExpiredCredentialsException, InvalidCredentialsException, MissingPermissionsException from .editions import DuplicateInsertException from .parsing import MalformedUUIDError +from .projects import StudentInConflictException, FailedToAddProjectRoleException from .register import FailedToAddNewUserException from .webhooks import WebhookProcessException @@ -79,3 +80,17 @@ def failed_to_add_new_user_exception(_request: Request, _exception: FailedToAddN status_code=status.HTTP_400_BAD_REQUEST, content={'message': 'Something went wrong while creating a new user'} ) + + @app.exception_handler(StudentInConflictException) + def student_in_conflict_exception(_request: Request, _exception: StudentInConflictException): + return JSONResponse( + status_code=status.HTTP_409_CONFLICT, + content={'message': 'Resolve the conflict this student is in before confirming their role'} + ) + + @app.exception_handler(FailedToAddProjectRoleException) + def student_in_conflict_exception(_request: Request, _exception: FailedToAddProjectRoleException): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={'message': 'Something went wrong while adding this student to the project'} + ) diff --git a/backend/src/app/exceptions/projects.py b/backend/src/app/exceptions/projects.py new file mode 100644 index 000000000..5ae7702d3 --- /dev/null +++ b/backend/src/app/exceptions/projects.py @@ -0,0 +1,10 @@ +class StudentInConflictException(Exception): + """ + Exception raised when a project_role of a student can't be confirmed because they are part of a conflict + """ + + +class FailedToAddProjectRoleException(Exception): + """ + Exception raised when a projct_role can't be added for some reason + """ diff --git a/backend/src/app/logic/projects_students.py b/backend/src/app/logic/projects_students.py index dea58d6c3..430fc5987 100644 --- a/backend/src/app/logic/projects_students.py +++ b/backend/src/app/logic/projects_students.py @@ -1,8 +1,11 @@ from sqlalchemy.orm import Session +from src.app.exceptions.projects import StudentInConflictException, FailedToAddProjectRoleException +from src.app.logic.projects import logic_get_conflicts +from src.app.schemas.projects import ConflictStudentList from src.database.crud.projects_students import db_remove_student_project, db_add_student_project, \ - db_change_project_role -from src.database.models import Project + db_change_project_role, db_confirm_project_role +from src.database.models import Project, ProjectRole, Student, Skill def logic_remove_student_project(db: Session, project: Project, student_id: int): @@ -12,9 +15,48 @@ def logic_remove_student_project(db: Session, project: Project, student_id: int) def logic_add_student_project(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): """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 project requires the skill + project = db.query(Project).where(Project.project_id == project.project_id).one() + if skill not in project.skills: + raise FailedToAddProjectRoleException + db_add_student_project(db, project, student_id, skill_id, drafter_id) def logic_change_project_role(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): """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 project requires the skill + project = db.query(Project).where(Project.project_id == project.project_id).one() + if skill not in project.skills: + raise FailedToAddProjectRoleException + db_change_project_role(db, project, student_id, skill_id, drafter_id) + + +def logic_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_get_conflicts(db, project.edition) + for conflict in conflict_list.conflict_students: + if conflict.student.student_id == student_id: + raise StudentInConflictException + + db_confirm_project_role(db, project, student_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 6ba005853..c0fe66e2c 100644 --- a/backend/src/app/routers/editions/projects/students/projects_students.py +++ b/backend/src/app/routers/editions/projects/students/projects_students.py @@ -3,13 +3,13 @@ from starlette import status from starlette.responses import Response +from src.app.logic.projects_students import logic_remove_student_project, logic_add_student_project, \ + logic_change_project_role, logic_confirm_project_role from src.app.routers.tags import Tags from src.app.schemas.projects import InputStudentRole -from src.app.utils.dependencies import get_project +from src.app.utils.dependencies import get_project, require_admin from src.database.database import get_session from src.database.models import Project -from src.app.logic.projects_students import logic_remove_student_project, logic_add_student_project, \ - logic_change_project_role project_students_router = APIRouter(prefix="/students", tags=[Tags.PROJECTS, Tags.STUDENTS]) @@ -41,3 +41,14 @@ async def add_student_to_project(student_id: int, input_sr: InputStudentRole, db 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, input_sr.drafter_id) + + +@project_students_router.post("/{student_id}/confirm", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, + dependencies=[Depends(require_admin)]) +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) diff --git a/backend/src/database/crud/projects_students.py b/backend/src/database/crud/projects_students.py index 5ba5a79e5..49489de04 100644 --- a/backend/src/database/crud/projects_students.py +++ b/backend/src/database/crud/projects_students.py @@ -6,7 +6,7 @@ def db_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() + student_id).where(ProjectRole.project == project).one() db.delete(proj_role) db.commit() @@ -38,3 +38,11 @@ def db_change_project_role(db: Session, project: Project, student_id: int, skill proj_role.drafter_id = drafter_id proj_role.skill_id = skill_id db.commit() + + +def db_confirm_project_role(db: Session, project: Project, student_id: int): + proj_role = db.query(ProjectRole).where(ProjectRole.student_id == student_id) \ + .where(ProjectRole.project == project).one() + + proj_role.definitive = True + db.commit() 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 09329142b..59053606b 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 @@ -4,6 +4,7 @@ from starlette import status from src.database.models import Edition, Project, User, Skill, ProjectRole, Student +from tests.utils.authorization import AuthClient @pytest.fixture @@ -11,29 +12,31 @@ 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="project3", 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") + skill4: Skill = Skill(name="skill4", description="something about skill4") database_session.add(skill1) database_session.add(skill2) database_session.add(skill3) + database_session.add(skill4) + project1 = Project(name="project1", edition=edition, number_of_students=2, skills=[skill1, skill2, skill3, skill4]) + 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]) + 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]) + 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]) + wants_to_be_student_coach=False, edition=edition, skills=[skill2, skill3, skill4]) database_session.add(student01) database_session.add(student02) database_session.add(student03) @@ -42,7 +45,7 @@ def database_with_data(database_session: Session) -> Session: 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") + student=student02, project=project1, skill=skill2, drafter=user, argumentation="argmunet") database_session.add(project_role1) database_session.add(project_role2) database_session.add(project_role3) @@ -60,7 +63,7 @@ def current_edition(database_with_data: Session) -> Edition: def test_add_student_project(database_with_data: Session, test_client: TestClient): """tests add a student to a project""" resp = test_client.post( - "/editions/1/projects/1/students/3", json={"skill_id": 1, "drafter_id": 1}) + "/editions/ed2022/projects/1/students/3", json={"skill_id": 3, "drafter_id": 1}) assert resp.status_code == status.HTTP_201_CREATED @@ -68,7 +71,7 @@ def test_add_student_project(database_with_data: Session, test_client: TestClien json = response2.json() assert len(json['projects'][0]['projectRoles']) == 3 - assert json['projects'][0]['projectRoles'][2]['skillId'] == 1 + assert json['projects'][0]['projectRoles'][2]['skillId'] == 3 def test_add_ghost_student_project(database_with_data: Session, test_client: TestClient): @@ -76,12 +79,12 @@ def test_add_ghost_student_project(database_with_data: Session, test_client: Tes student10: list[Student] = database_with_data.query( Student).where(Student.student_id == 10).all() assert len(student10) == 0 - response = test_client.get('/editions/1/projects/1') + response = test_client.get('/editions/ed2022/projects/1') json = response.json() assert len(json['projectRoles']) == 2 resp = test_client.post( - "/editions/ed2022/projects/1/students/10", json={"skill_id": 1, "drafter_id": 1}) + "/editions/ed2022/projects/1/students/10", json={"skill_id": 3, "drafter_id": 1}) assert resp.status_code == status.HTTP_404_NOT_FOUND response = test_client.get('/editions/ed2022/projects/1') @@ -118,7 +121,7 @@ def test_add_student_project_ghost_drafter(database_with_data: Session, test_cli assert len(json['projectRoles']) == 2 resp = test_client.post( - "/editions/ed2022/projects/1/students/3", json={"skill_id": 1, "drafter_id": 10}) + "/editions/ed2022/projects/1/students/3", json={"skill_id": 3, "drafter_id": 10}) assert resp.status_code == status.HTTP_404_NOT_FOUND response = test_client.get('/editions/ed2022/projects/1') @@ -159,7 +162,7 @@ def test_add_incomplete_data_student_project(database_session: Session, test_cli def test_change_student_project(database_with_data: Session, test_client: TestClient): """test change a student project""" resp1 = test_client.patch( - "/editions/ed2022/projects/1/students/1", json={"skill_id": 2, "drafter_id": 1}) + "/editions/ed2022/projects/1/students/1", json={"skill_id": 4, "drafter_id": 1}) assert resp1.status_code == status.HTTP_204_NO_CONTENT @@ -167,7 +170,7 @@ def test_change_student_project(database_with_data: Session, test_client: TestCl json = response2.json() assert len(json['projects'][0]['projectRoles']) == 2 - assert json['projects'][0]['projectRoles'][0]['skillId'] == 2 + assert json['projects'][0]['projectRoles'][0]['skillId'] == 4 def test_change_incomplete_data_student_project(database_with_data: Session, test_client: TestClient): @@ -194,7 +197,7 @@ def test_change_ghost_student_project(database_with_data: Session, test_client: assert len(json['projectRoles']) == 2 resp = test_client.patch( - "/editions/ed2022/projects/1/students/10", json={"skill_id": 1, "drafter_id": 1}) + "/editions/ed2022/projects/1/students/10", json={"skill_id": 4, "drafter_id": 1}) assert resp.status_code == status.HTTP_404_NOT_FOUND response = test_client.get('/editions/ed2022/projects/1') @@ -231,7 +234,7 @@ def test_change_student_project_ghost_drafter(database_with_data: Session, test_ assert len(json['projectRoles']) == 2 resp = test_client.patch( - "/editions/ed2022/projects/1/students/3", json={"skill_id": 1, "drafter_id": 10}) + "/editions/ed2022/projects/1/students/3", json={"skill_id": 4, "drafter_id": 10}) assert resp.status_code == status.HTTP_404_NOT_FOUND response = test_client.get('/editions/ed2022/projects/1') @@ -283,3 +286,53 @@ def test_get_conflicts(database_with_data: Session, test_client: TestClient): assert json['conflictStudents'][0]['student']['studentId'] == 1 assert len(json['conflictStudents'][0]['projects']) == 2 assert json['editionName'] == "ed2022" + + +def test_add_student_same_project_role(database_with_data: Session, test_client: TestClient): + """Two different students can't have the same project_role""" + resp = test_client.post( + "/editions/ed2022/projects/1/students/3", json={"skill_id": 2, "drafter_id": 1}) + + assert resp.status_code == status.HTTP_400_BAD_REQUEST + + +def test_add_student_project_wrong_project_skill(database_with_data: Session, test_client: TestClient): + """A project_role can't be created if the project doesn't require the skill""" + resp = test_client.post( + "/editions/ed2022/projects/3/students/3", json={"skill_id": 4, "drafter_id": 1}) + + assert resp.status_code == status.HTTP_400_BAD_REQUEST + + +def test_add_student_project_wrong_student_skill(database_with_data: Session, test_client: TestClient): + """A project_role can't be created if the student doesn't have the skill""" + resp = test_client.post( + "/editions/ed2022/projects/1/students/2", json={"skill_id": 1, "drafter_id": 1}) + + 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""" + resp = auth_client.post( + "/editions/ed2022/projects/1/students/3", json={"skill_id": 3, "drafter_id": 1}) + + assert resp.status_code == status.HTTP_201_CREATED + + auth_client.admin() + 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 From 92aef32a9f320cea5cc3fe2bf3699f33a32fc3a7 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 1 Apr 2022 15:23:56 +0200 Subject: [PATCH 170/536] student search implemented --- backend/src/app/logic/students.py | 15 +++- backend/src/app/schemas/students.py | 4 +- .../test_students/test_students.py | 83 ++++++++++++++++--- 3 files changed, 88 insertions(+), 14 deletions(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index 2c91bacf4..de3fd770b 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -1,8 +1,9 @@ from sqlalchemy.orm import Session +from sqlalchemy.orm.exc import NoResultFound from src.app.schemas.students import NewDecision from src.database.crud.students import set_definitive_decision_on_student, delete_student, get_students -from src.database.models import Edition, Student +from src.database.models import Edition, Student, Skill from src.app.schemas.students import ReturnStudentList, ReturnStudent, CommonQueryParams @@ -18,9 +19,17 @@ def remove_student(db: Session, student: Student) -> None: def get_students_search(db: Session, edition: Edition, commons: CommonQueryParams) -> ReturnStudentList: """return all students""" - # TODO: skill_ids to skill's + # TODO: use function in crud/skills.py + if commons.skill_ids: + skills: list[Skill] = db.query(Skill).where( + Skill.skill_id.in_(commons.skill_ids)).all() + if not skills: #TODO: should this be a costum error with a message or not? + raise NoResultFound + else: + skills = [] students = get_students(db, edition, first_name=commons.first_name, - last_name=commons.last_name, alumni=commons.alumni, student_coach=commons.student_coach) + last_name=commons.last_name, alumni=commons.alumni, + student_coach=commons.student_coach, skills=skills) return ReturnStudentList(students=students) diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index 2bd5e56d9..d21cc4052 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -1,3 +1,5 @@ +from fastapi import Query + from src.app.schemas.webhooks import CamelCaseModel from src.database.enums import DecisionEnum @@ -46,7 +48,7 @@ class CommonQueryParams: """search query paramaters""" def __init__(self, first_name: str = "", last_name: str = "", alumni: bool = False, - student_coach: bool = False, skill_ids: list[int] = None) -> None: + student_coach: bool = False, skill_ids: list[int] = Query([])) -> None: """init""" self.first_name = first_name self.last_name = last_name 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 a11333fa6..374dc9b1f 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 @@ -55,7 +55,7 @@ def database_with_data(database_session: Session) -> Session: email_address="josvermeulen@mail.com", phone_number="0487/86.24.45", alumni=True, 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=True, + 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]) database_session.add(student01) @@ -168,7 +168,8 @@ def test_delete_student_coach(database_with_data: Session, test_client: TestClie """tests""" assert test_client.delete("/editions/1/students/2", headers={ "Authorization": auth_coach1}).status_code == status.HTTP_403_FORBIDDEN - students: Student = database_with_data.query(Student).where(Student.student_id == 1).all() + students: Student = database_with_data.query( + Student).where(Student.student_id == 1).all() assert len(students) == 1 @@ -176,7 +177,8 @@ def test_delete_ghost(database_with_data: Session, test_client: TestClient, auth """tests""" assert test_client.delete("/editions/1/students/100", headers={"Authorization": auth_admin}).status_code == status.HTTP_404_NOT_FOUND - students: Student = database_with_data.query(Student).where(Student.student_id == 1).all() + students: Student = database_with_data.query( + Student).where(Student.student_id == 1).all() assert len(students) == 1 @@ -184,36 +186,97 @@ def test_delete(database_with_data: Session, test_client: TestClient, auth_admin """tests""" assert test_client.delete("/editions/1/students/1", headers={"Authorization": auth_admin}).status_code == status.HTTP_204_NO_CONTENT - students: Student = database_with_data.query(Student).where(Student.student_id == 1).all() + students: Student = database_with_data.query( + Student).where(Student.student_id == 1).all() assert len(students) == 0 def test_get_student_by_id_no_autorization(database_with_data: Session, test_client: TestClient): """tests""" assert test_client.get("/editions/1/students/1", - headers={"Authorization": "auth_admin"}).status_code == status.HTTP_401_UNAUTHORIZED + headers={"Authorization": "auth_admin"}).status_code == status.HTTP_401_UNAUTHORIZED def test_get_student_by_id(database_with_data: Session, test_client: TestClient, auth_admin: str): """tests""" assert test_client.get("/editions/1/students/1", - headers={"Authorization": auth_admin}).status_code == status.HTTP_200_OK + headers={"Authorization": auth_admin}).status_code == status.HTTP_200_OK def test_get_students_no_autorization(database_with_data: Session, test_client: TestClient): """tests""" assert test_client.get("/editions/1/students/", - headers={"Authorization": "auth_admin"}).status_code == status.HTTP_401_UNAUTHORIZED + headers={"Authorization": "auth_admin"}).status_code == status.HTTP_401_UNAUTHORIZED def test_get_all_students(database_with_data: Session, test_client: TestClient, auth_admin: str): """tests""" response = test_client.get("/editions/1/students/", - headers={"Authorization": auth_admin}) + headers={"Authorization": auth_admin}) assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == 2 + def test_get_first_name_students(database_with_data: Session, test_client: TestClient, auth_admin: str): """tests""" - response = test_client.get("/editions/1/students/?first_name='jos'", - headers={"Authorization": auth_admin}) + response = test_client.get("/editions/1/students/?first_name=Jos", + headers={"Authorization": auth_admin}) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == 1 + + +def test_get_last_name_students(database_with_data: Session, test_client: TestClient, auth_admin: str): + """tests""" + response = test_client.get("/editions/1/students/?last_name=Vermeulen", + headers={"Authorization": auth_admin}) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == 1 + + +def test_get_alumni_students(database_with_data: Session, test_client: TestClient, auth_admin: str): + """tests""" + response = test_client.get("/editions/1/students/?alumni=true", + headers={"Authorization": auth_admin}) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == 1 + + +def test_get_student_coach_students(database_with_data: Session, test_client: TestClient, auth_admin: str): + """tests""" + response = test_client.get("/editions/1/students/?student_coach=true", + headers={"Authorization": auth_admin}) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == 1 + + +def test_get_one_skill_students(database_with_data: Session, test_client: TestClient, auth_admin: str): + """tests""" + response = test_client.get("/editions/1/students/?skill_ids=1", + headers={"Authorization": auth_admin}) + 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, test_client: TestClient, auth_admin: str): + """tests""" + response = test_client.get("/editions/1/students/?skill_ids=4&skill_ids=5", + headers={"Authorization": auth_admin}) 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, test_client: TestClient, auth_admin: str): + """tests""" + response = test_client.get("/editions/1/students/?skill_ids=4&skill_ids=6", + headers={"Authorization": auth_admin}) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == 0 + + +def test_get_ghost_skill_students(database_with_data: Session, test_client: TestClient, auth_admin: str): + """tests""" + response = test_client.get("/editions/1/students/?skill_ids=100", + headers={"Authorization": auth_admin}) + assert response.status_code == status.HTTP_404_NOT_FOUND From edb9ccf1e511514a24a40bf055012fb73b690088 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Fri, 1 Apr 2022 17:14:28 +0200 Subject: [PATCH 171/536] started working on projects page --- .../ProjectCard/ProjectCard.tsx | 24 +++++++++++++ .../ProjectsComponents/ProjectCard/index.ts | 1 + .../ProjectsComponents/ProjectCard/styles.ts | 36 +++++++++++++++++++ .../components/ProjectsComponents/index.ts | 1 + .../src/views/ProjectsPage/ProjectsPage.tsx | 32 ++++++++++++++++- frontend/src/views/ProjectsPage/styles.ts | 8 +++++ 6 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx create mode 100644 frontend/src/components/ProjectsComponents/ProjectCard/index.ts create mode 100644 frontend/src/components/ProjectsComponents/ProjectCard/styles.ts create mode 100644 frontend/src/components/ProjectsComponents/index.ts create mode 100644 frontend/src/views/ProjectsPage/styles.ts diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx new file mode 100644 index 000000000..d0d5e1231 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -0,0 +1,24 @@ +import { CardContainer, CoachesContainer, CoachContainer, Delete } from "./styles"; + +export default function ProjectCard({ + name, + client, + coaches, +}: { + name: string; + client: string; + coaches: string[]; +}) { + return ( + +

{name}

+

{client}

+ + {coaches.map((element, index) => ( + {element} + ))} + + X +
+ ); +} diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/index.ts b/frontend/src/components/ProjectsComponents/ProjectCard/index.ts new file mode 100644 index 000000000..b45666a95 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/ProjectCard/index.ts @@ -0,0 +1 @@ +export { default } from "./ProjectCard"; diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts new file mode 100644 index 000000000..bcbe2fdba --- /dev/null +++ b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts @@ -0,0 +1,36 @@ +import styled from "styled-components"; + +export const CardContainer = styled.div` + border: 2px solid #1a1a36; + border-radius: 20px; + margin: 20px; + margin-bottom: 5px; + padding: 20px 50px 20px 20px; + background-color: #323252; + box-shadow: 5px 5px 15px #131329; +`; + +export const CoachesContainer = styled.div` + display: flex; + margin-top: 20px; +`; + +export const CoachContainer = styled.div` + background-color: #1a1a36; + border-radius: 10px; + margin-right: 10px; + text-align: center; + padding: 10px; + max-width: 50%; + text-overflow: ellipsis; + overflow: hidden; +`; + +export const Delete = styled.button` + background-color: #f14a3b; + padding: 5px 10px; + border: 0; + border-radius: 5px; + margin-top: 20px; + margin-left: 100%; +`; diff --git a/frontend/src/components/ProjectsComponents/index.ts b/frontend/src/components/ProjectsComponents/index.ts new file mode 100644 index 000000000..09548c092 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/index.ts @@ -0,0 +1 @@ +export { default as ProjectCard } from "./ProjectCard"; diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index d6469892f..4b6f31773 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -1,8 +1,38 @@ import React from "react"; import "./ProjectsPage.css"; +import { ProjectCard } from "../../components/ProjectsComponents"; + +import { CardsGrid } from "./styles"; + function ProjectPage() { - return
This is the projects page
; + return ( +
+ + + + + + + + + + + +
+ ); } export default ProjectPage; diff --git a/frontend/src/views/ProjectsPage/styles.ts b/frontend/src/views/ProjectsPage/styles.ts new file mode 100644 index 000000000..3c93d10af --- /dev/null +++ b/frontend/src/views/ProjectsPage/styles.ts @@ -0,0 +1,8 @@ +import styled from "styled-components"; + +export const CardsGrid = styled.div` + display: grid; + grid-gap: 5px; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-auto-flow: dense; +`; From f5ee85d03a626ad1d7b6de075f6d714127d2a1c4 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 2 Apr 2022 13:15:01 +0200 Subject: [PATCH 172/536] moved delete button to top right api call for get projects --- .../ProjectCard/ProjectCard.tsx | 12 +++++++++--- .../ProjectsComponents/ProjectCard/styles.ts | 12 +++++++++--- frontend/src/utils/api/projects.ts | 18 ++++++++++++++++++ .../src/views/ProjectsPage/ProjectsPage.tsx | 18 +++++++++++++++++- 4 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 frontend/src/utils/api/projects.ts diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index d0d5e1231..9e5fae533 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -1,4 +1,4 @@ -import { CardContainer, CoachesContainer, CoachContainer, Delete } from "./styles"; +import { CardContainer, CoachesContainer, CoachContainer, Delete, TitleContainer } from "./styles"; export default function ProjectCard({ name, @@ -11,14 +11,20 @@ export default function ProjectCard({ }) { return ( -

{name}

+ +
+

{name}

+
+ + X +
+

{client}

{coaches.map((element, index) => ( {element} ))} - X
); } diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts index bcbe2fdba..86bf7cc25 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts +++ b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts @@ -5,11 +5,16 @@ export const CardContainer = styled.div` border-radius: 20px; margin: 20px; margin-bottom: 5px; - padding: 20px 50px 20px 20px; + padding: 20px 20px 20px 20px; background-color: #323252; box-shadow: 5px 5px 15px #131329; `; +export const TitleContainer = styled.div` + display: flex; + justify-content: space-between; +`; + export const CoachesContainer = styled.div` display: flex; margin-top: 20px; @@ -22,6 +27,7 @@ export const CoachContainer = styled.div` text-align: center; padding: 10px; max-width: 50%; + min-width: 20%; text-overflow: ellipsis; overflow: hidden; `; @@ -31,6 +37,6 @@ export const Delete = styled.button` padding: 5px 10px; border: 0; border-radius: 5px; - margin-top: 20px; - margin-left: 100%; + max-height: 35px; + margin-left: 5%; `; diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts new file mode 100644 index 000000000..bf9ce358a --- /dev/null +++ b/frontend/src/utils/api/projects.ts @@ -0,0 +1,18 @@ +import axios from "axios"; +import { axiosInstance } from "./api"; + +export async function getProjects(edition: string) { + try { + const response = await axiosInstance.get("/editions/" + edition + "/projects"); + console.log(response); + + const projects = response.data; + return projects; + } catch (error) { + if (axios.isAxiosError(error)) { + return false; + } else { + throw error; + } + } +} diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index 4b6f31773..64287da75 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -1,4 +1,5 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; +import { getProjects } from "../../utils/api/projects"; import "./ProjectsPage.css"; import { ProjectCard } from "../../components/ProjectsComponents"; @@ -6,6 +7,21 @@ import { ProjectCard } from "../../components/ProjectsComponents"; import { CardsGrid } from "./styles"; function ProjectPage() { + const [projects, setProjects] = useState(); + + useEffect(() => { + async function callProjects() { + const response = await getProjects("1"); + if (response) { + console.log(response); + setProjects(response); + } + } + if (!projects) { + callProjects(); + } else console.log("hello"); + }); + return (
From ca5cc52769e662268a469994f089035ac6fef00e Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sat, 2 Apr 2022 14:46:12 +0200 Subject: [PATCH 173/536] Coaches list --- frontend/package.json | 1 + frontend/src/utils/api/users/coaches.ts | 38 ++++ .../utils/api/{users.ts => users/requests.ts} | 40 +---- frontend/src/utils/api/users/users.ts | 29 +++ .../src/views/UsersPage/Coaches/Coaches.tsx | 167 ++++++++++++++++++ frontend/src/views/UsersPage/Coaches/index.ts | 1 + .../src/views/UsersPage/Coaches/styles.ts | 39 ++++ .../views/UsersPage/InviteUser/InviteUser.tsx | 7 +- .../PendingRequests/PendingRequests.tsx | 32 ++-- .../views/UsersPage/PendingRequests/styles.ts | 8 +- frontend/src/views/UsersPage/UsersPage.tsx | 19 +- frontend/yarn.lock | 5 + 12 files changed, 321 insertions(+), 65 deletions(-) create mode 100644 frontend/src/utils/api/users/coaches.ts rename frontend/src/utils/api/{users.ts => users/requests.ts} (53%) create mode 100644 frontend/src/utils/api/users/users.ts create mode 100644 frontend/src/views/UsersPage/Coaches/Coaches.tsx create mode 100644 frontend/src/views/UsersPage/Coaches/index.ts create mode 100644 frontend/src/views/UsersPage/Coaches/styles.ts diff --git a/frontend/package.json b/frontend/package.json index 224035f40..459e2ad6e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "react-router-dom": "^6.2.1", "react-scripts": "^5.0.0", "react-social-login-buttons": "^3.6.0", + "reactjs-popup": "^2.0.5", "styled-components": "^5.3.3", "typescript": "^4.4.2", "web-vitals": "^2.1.0" diff --git a/frontend/src/utils/api/users/coaches.ts b/frontend/src/utils/api/users/coaches.ts new file mode 100644 index 000000000..880e6d3e7 --- /dev/null +++ b/frontend/src/utils/api/users/coaches.ts @@ -0,0 +1,38 @@ +import { User } from "./users"; + +export interface GetCoachesResponse { + coaches: User[]; +} + +export async function getCoaches(edition: string): Promise { + const data = { + coaches: [ + { + id: 3, + name: "Bert", + email: "bert@mail.be", + admin: false, + }, + { + id: 4, + name: "Tiebe", + email: "tiebe@mail.be", + admin: false, + }, + ], + }; + + // eslint-disable-next-line promise/param-names + const delay = () => new Promise(res => setTimeout(res, 100)); + await delay(); + + return data; +} + +export async function removeCoachFromEdition(userId: number, edition: string) { + alert("remove " + userId + " from " + edition); +} + +export async function removeCoachFromAllEditions(userId: Number) { + alert("remove " + userId + " from all editions"); +} diff --git a/frontend/src/utils/api/users.ts b/frontend/src/utils/api/users/requests.ts similarity index 53% rename from frontend/src/utils/api/users.ts rename to frontend/src/utils/api/users/requests.ts index 41853062a..a1716e187 100644 --- a/frontend/src/utils/api/users.ts +++ b/frontend/src/utils/api/users/requests.ts @@ -1,12 +1,4 @@ -import axios from "axios"; -import { axiosInstance } from "./api"; - -export interface User { - id: Number; - name: string; - email: string; - admin: boolean; -} +import { User } from "./users"; export interface Request { id: number; @@ -17,7 +9,7 @@ export interface GetRequestsResponse { requests: Request[]; } -export async function getRequests(edition: string | undefined): Promise { +export async function getRequests(edition: string): Promise { const data = { requests: [ { @@ -59,30 +51,10 @@ export async function getRequests(edition: string | undefined): Promise { - try { - await axiosInstance - .post(`/editions/${edition}/invites/`, { email: email }) - .then(response => { - return response.data.mailTo; - }); - } catch (error) { - if (axios.isAxiosError(error)) { - return error.message; - } else { - throw error; - } - } - return ""; -} - -export async function acceptRequest(requestId: Number) { - alert("Accept"); +export async function acceptRequest(requestId: number) { + alert("Accept " + requestId); } -export async function rejectRequest(requestId: Number) { - alert("Reject"); +export async function rejectRequest(requestId: number) { + alert("Reject " + requestId); } diff --git a/frontend/src/utils/api/users/users.ts b/frontend/src/utils/api/users/users.ts new file mode 100644 index 000000000..fdc5daa8c --- /dev/null +++ b/frontend/src/utils/api/users/users.ts @@ -0,0 +1,29 @@ +import axios from "axios"; +import { axiosInstance } from "../api"; + +export interface User { + id: number; + name: string; + email: string; + admin: boolean; +} + +/** + * Get invite link for given email and edition + */ +export async function getInviteLink(edition: string | undefined, email: string): Promise { + try { + await axiosInstance + .post(`/editions/${edition}/invites/`, { email: email }) + .then(response => { + return response.data.mailTo; + }); + } catch (error) { + if (axios.isAxiosError(error)) { + return error.message; + } else { + throw error; + } + } + return ""; +} diff --git a/frontend/src/views/UsersPage/Coaches/Coaches.tsx b/frontend/src/views/UsersPage/Coaches/Coaches.tsx new file mode 100644 index 000000000..d5ec75481 --- /dev/null +++ b/frontend/src/views/UsersPage/Coaches/Coaches.tsx @@ -0,0 +1,167 @@ +import React, { useEffect, useState } from "react"; +import { CoachesTitle, CoachesContainer, CoachesTable, ModalContent } from "./styles"; +import { User } from "../../../utils/api/users/users"; +import { SearchInput, SpinnerContainer } from "../PendingRequests/styles"; +import { Button, Modal, Spinner } from "react-bootstrap"; +import { + getCoaches, + removeCoachFromAllEditions, + removeCoachFromEdition, +} from "../../../utils/api/users/coaches"; + +function CoachesHeader() { + return Coaches; +} + +function CoachFilter(props: { + search: boolean; + searchTerm: string; + filter: (key: string) => void; +}) { + return props.filter(e.target.value)} />; +} + +function RemoveCoach(props: { coach: User; edition: string }) { + const [show, setShow] = useState(false); + + const handleClose = () => setShow(false); + const handleShow = () => setShow(true); + + return ( + <> + + + + + + Remove Coach + + +

{props.coach.name}

+ {props.coach.email} +
+ + + + + +
+
+ + ); +} + +function CoachItem(props: { coach: User; edition: string }) { + return ( + + {props.coach.name} + {props.coach.email} + + + + + ); +} + +function CoachesList(props: { coaches: User[]; loading: boolean; edition: string }) { + if (props.loading) { + return ( + + + + ); + } else if (props.coaches.length === 0) { + return
No coaches for this edition
; + } + + const body = ( + + {props.coaches.map(coach => ( + + ))} + + ); + + return ( + + + + Name + Email + Remove from edition + + + {body} + + ); +} + +export default function Coaches(props: { edition: string }) { + const [allCoaches, setAllCoaches] = useState([]); + const [coaches, setCoaches] = useState([]); + const [gettingCoaches, setGettingCoaches] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [gotData, setGotData] = useState(false); + + useEffect(() => { + if (!gotData) { + getCoaches(props.edition) + .then(response => { + setCoaches(response.coaches); + setAllCoaches(response.coaches); + setGettingCoaches(false); + setGotData(true); + }) + .catch(function (error: any) { + console.log(error); + setGettingCoaches(false); + }); + } + }); + + const filter = (word: string) => { + setSearchTerm(word); + const newCoaches: User[] = []; + for (const coach of allCoaches) { + if ( + coach.name.toUpperCase().includes(word.toUpperCase()) || + coach.email.toUpperCase().includes(word.toUpperCase()) + ) { + newCoaches.push(coach); + } + } + setCoaches(newCoaches); + }; + + return ( + + + 0} + searchTerm={searchTerm} + filter={word => filter(word)} + /> + + + ); +} diff --git a/frontend/src/views/UsersPage/Coaches/index.ts b/frontend/src/views/UsersPage/Coaches/index.ts new file mode 100644 index 000000000..ef8ea8035 --- /dev/null +++ b/frontend/src/views/UsersPage/Coaches/index.ts @@ -0,0 +1 @@ +export { default as Coaches } from "./Coaches"; diff --git a/frontend/src/views/UsersPage/Coaches/styles.ts b/frontend/src/views/UsersPage/Coaches/styles.ts new file mode 100644 index 000000000..1f8970bd5 --- /dev/null +++ b/frontend/src/views/UsersPage/Coaches/styles.ts @@ -0,0 +1,39 @@ +import styled from "styled-components"; +import { Table } from "react-bootstrap"; + +export const CoachesContainer = styled.div` + width: 50%; + min-width: 600px; + margin: 10px auto auto; +`; + +export const CoachesTitle = styled.div` + padding-bottom: 3px; + padding-left: 3px; + width: 100px; + font-size: 25px; +`; + +export const RemoveFromEditionButton = styled.button` + background-color: var(--osoc_red); + margin-left: 3px; + padding-bottom: 3px; + padding-left: 3px; + padding-right: 3px; +`; + +export const CoachesTable = styled(Table)``; + +export const PopupDiv = styled.div` + background-color: var(--osoc_red); + width: 200px; + height: 100px; + position: absolute; + right: 0; + top: 0; +`; + +export const ModalContent = styled.div` + border: 3px solid var(--osoc_red); + background-color: var(--osoc_blue); +`; diff --git a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx index e0e4b7050..22b4339b5 100644 --- a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx +++ b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { getInviteLink } from "../../../utils/api/users"; +import { getInviteLink } from "../../../utils/api/users/users"; import "./InviteUsers.css"; import { InviteInput, InviteButton, Loader, InviteContainer, Link, Error } from "./styles"; @@ -33,7 +33,7 @@ function LinkDiv(props: { link: string }) { return linkDiv; } -export default function InviteUser(props: { edition: string | undefined }) { +export default function InviteUser(props: { edition: string }) { const [email, setEmail] = useState(""); const [valid, setValid] = useState(true); const [errorMessage, setErrorMessage] = useState(""); @@ -50,7 +50,7 @@ export default function InviteUser(props: { edition: string | undefined }) { const sendInvite = async () => { if (/[^@\s]+@[^@\s]+\.[^@\s]+/.test(email)) { setLoading(true); - const ding = await getInviteLink("props.edition", email); + const ding = await getInviteLink(props.edition, email); setLink(ding); setLoading(false); // TODO: fix email stuff @@ -62,7 +62,6 @@ export default function InviteUser(props: { edition: string | undefined }) { return (
-
{props.edition}
Requests {props.open ? "opened" : "closed"}; + return Requests; } -function RequestFilter(props: { - search: boolean; - searchTerm: string; - filter: (key: string) => void; -}) { +function RequestFilter(props: { searchTerm: string; filter: (key: string) => void }) { return props.filter(e.target.value)} />; } -function AcceptReject(props: { request_id: number }) { +function AcceptReject(props: { requestId: number }) { return (
- acceptRequest(props.request_id)}>Accept - rejectRequest(props.request_id)}>Reject + acceptRequest(props.requestId)}>Accept + rejectRequest(props.requestId)}>Reject
); } @@ -39,7 +40,7 @@ function RequestItem(props: { request: Request }) { {props.request.user.name} {props.request.user.email} - + ); @@ -78,7 +79,7 @@ function RequestsList(props: { requests: Request[]; loading: boolean }) { ); } -export default function PendingRequests(props: { edition: string | undefined }) { +export default function PendingRequests(props: { edition: string }) { const [allRequests, setAllRequests] = useState([]); const [requests, setRequests] = useState([]); const [gettingRequests, setGettingRequests] = useState(true); @@ -116,7 +117,6 @@ export default function PendingRequests(props: { edition: string | undefined }) setRequests(newRequests); }; - // @ts-ignore return ( setOpen(true)} onClose={() => setOpen(false)} > - 0} - searchTerm={searchTerm} - filter={word => filter(word)} - /> + filter(word)} /> diff --git a/frontend/src/views/UsersPage/PendingRequests/styles.ts b/frontend/src/views/UsersPage/PendingRequests/styles.ts index 17e81b40a..6c004c5ff 100644 --- a/frontend/src/views/UsersPage/PendingRequests/styles.ts +++ b/frontend/src/views/UsersPage/PendingRequests/styles.ts @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { Table } from "react-bootstrap"; +import { Table, Button } from "react-bootstrap"; export const RequestHeaderTitle = styled.div` padding-bottom: 3px; @@ -27,15 +27,17 @@ export const PendingRequestsContainer = styled.div` margin: 10px auto auto; `; -export const AcceptButton = styled.button` +export const AcceptButton = styled(Button)` background-color: var(--osoc_green); + color: black; padding-bottom: 3px; padding-left: 3px; padding-right: 3px; `; -export const RejectButton = styled.button` +export const RejectButton = styled(Button)` background-color: var(--osoc_red); + color: black; margin-left: 3px; padding-bottom: 3px; padding-left: 3px; diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index fab2469bc..3740965ff 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -1,16 +1,23 @@ import React from "react"; import { InviteUser } from "./InviteUser"; import { PendingRequests } from "./PendingRequests"; +import { Coaches } from "./Coaches"; import { useParams } from "react-router-dom"; function UsersPage() { const params = useParams(); - return ( -
- - -
- ); + + if (params.edition === undefined) { + return
Error
; + } else { + return ( +
+ + + +
+ ); + } } export default UsersPage; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 03dc41af5..5232bfe5c 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -7520,6 +7520,11 @@ react@^17.0.2: loose-envify "^1.1.0" object-assign "^4.1.1" +reactjs-popup@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/reactjs-popup/-/reactjs-popup-2.0.5.tgz#588a74966bb126699429d739948e3448d7771eac" + 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" From 27a4ec7831052631650480d895bf5e39245e2b51 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sat, 2 Apr 2022 14:46:28 +0200 Subject: [PATCH 174/536] Fix typo --- .../test_routers/test_editions/test_projects/test_projects.py | 1 - 1 file changed, 1 deletion(-) 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 d5672a93d..f5f8978ac 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 @@ -264,7 +264,6 @@ def test_patch_wrong_project(database_session: Session, auth_client: AuthClient) response = \ auth_client.patch("/editions/ed2022/projects/1", - test_client.patch("/editions/ed2022/projects/1", json={"name": "patched", "skills": [], "partners": [], "coaches": []}) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY From 9abcb4934627dc3fcde8626e38ac08725bcf35f0 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 27 Mar 2022 23:22:11 +0200 Subject: [PATCH 175/536] Start working on authentication context provider --- frontend/src/App.tsx | 53 ++++++++----------- frontend/src/Router.tsx | 45 ++++++++++++++++ frontend/src/contexts/auth-context.ts | 30 +++++++++++ frontend/src/contexts/index.ts | 3 ++ frontend/src/data/enums/index.ts | 2 + frontend/src/data/enums/local-storage.ts | 6 +++ frontend/src/data/enums/role.ts | 8 +++ frontend/src/utils/local-storage/auth.ts | 16 ++++++ frontend/src/utils/local-storage/index.ts | 1 + .../VerifyingTokenPage/VerifyingTokenPage.tsx | 15 ++++++ .../src/views/VerifyingTokenPage/index.ts | 1 + 11 files changed, 149 insertions(+), 31 deletions(-) create mode 100644 frontend/src/Router.tsx create mode 100644 frontend/src/contexts/auth-context.ts create mode 100644 frontend/src/contexts/index.ts create mode 100644 frontend/src/data/enums/index.ts create mode 100644 frontend/src/data/enums/local-storage.ts create mode 100644 frontend/src/data/enums/role.ts create mode 100644 frontend/src/utils/local-storage/auth.ts create mode 100644 frontend/src/utils/local-storage/index.ts create mode 100644 frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx create mode 100644 frontend/src/views/VerifyingTokenPage/index.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 05c360e6f..83c08b2b0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,41 +1,32 @@ import React, { useState } from "react"; import "bootstrap/dist/css/bootstrap.min.css"; import "./App.css"; -import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; -import NavBar from "./components/navbar"; -import LoginPage from "./views/LoginPage"; -import StudentsPage from "./views/StudentsPage"; -import UsersPage from "./views/UsersPage"; -import ProjectsPage from "./views/ProjectsPage"; -import RegisterPage from "./views/RegisterPage"; -import ErrorPage from "./views/ErrorPage"; -import PendingPage from "./views/PendingPage"; -import Footer from "./components/Footer"; -import { Container, ContentWrapper } from "./app.styles"; +import { AuthContext, AuthContextState } from "./contexts"; +import Router from "./Router"; +import { Role } from "./data/enums"; +import { getToken } from "./utils/local-storage"; function App() { - const [token, setToken] = useState(""); + const [isLoggedIn, setIsLoggedIn] = useState(null); + const [role, setRole] = useState(null); + // Default value: check LocalStorage + const [token, setToken] = useState(getToken()); - return ( - - - + // Create AuthContext value + const authContextValue: AuthContextState = { + isLoggedIn: isLoggedIn, + setIsLoggedIn: setIsLoggedIn, + role: role, + setRole: setRole, + token: token, + setToken: setToken, + }; - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - -
Your token: {token}
-
- - + return ( + // AuthContext should be visible in the entire application + + + ); } diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx new file mode 100644 index 000000000..cbd7020e7 --- /dev/null +++ b/frontend/src/Router.tsx @@ -0,0 +1,45 @@ +import React, { useContext } from "react"; +import { AuthContext } from "./contexts"; +import VerifyingTokenPage from "./views/VerifyingTokenPage"; +import LoginPage from "./views/LoginPage"; +import { Container, ContentWrapper } from "./app.styles"; +import NavBar from "./components/navbar"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import RegisterPage from "./views/RegisterPage"; +import StudentsPage from "./views/StudentsPage/StudentsPage"; +import UsersPage from "./views/UsersPage"; +import ProjectsPage from "./views/ProjectsPage/ProjectsPage"; +import PendingPage from "./views/PendingPage"; +import ErrorPage from "./views/ErrorPage"; +import Footer from "./components/Footer"; + +export default function Router() { + const authContext = useContext(AuthContext); + + return authContext.isLoggedIn === null ? ( + // If verification hasn't completed, show nothing + ) : !authContext.isLoggedIn ? ( // If the user isn't logged in, show the login page + + ) : ( + // If the user IS logged in, render the actual app + + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + +
Your token: {authContext.token}
+
+ + + ); +} diff --git a/frontend/src/contexts/auth-context.ts b/frontend/src/contexts/auth-context.ts new file mode 100644 index 000000000..d9a55c85a --- /dev/null +++ b/frontend/src/contexts/auth-context.ts @@ -0,0 +1,30 @@ +/** Context hook to maintain the authentication state of the user **/ +import { Role } from "../data/enums"; +import React from "react"; +import { getToken } from "../utils/local-storage"; + +export interface AuthContextState { + isLoggedIn: boolean | null; + setIsLoggedIn: (value: boolean | null) => void; + role: Role | null; + setRole: (value: Role | null) => void; + token: string | null; + setToken: (value: string | null) => void; +} + +/** + * Create a placeholder default value for the state + */ +function authDefaultState(): AuthContextState { + return { + isLoggedIn: null, + setIsLoggedIn: (_: boolean | null) => {}, + role: null, + setRole: (_: Role | null) => {}, + token: getToken(), + setToken: (_: string | null) => {}, + }; +} + +// Set isLoggedIn to null on startup while we verify the token of the user +export const AuthContext = React.createContext(authDefaultState()); diff --git a/frontend/src/contexts/index.ts b/frontend/src/contexts/index.ts new file mode 100644 index 000000000..358311d76 --- /dev/null +++ b/frontend/src/contexts/index.ts @@ -0,0 +1,3 @@ +import type { AuthContextState } from "./auth-context"; +export type { AuthContextState }; +export { AuthContext } from "./auth-context"; diff --git a/frontend/src/data/enums/index.ts b/frontend/src/data/enums/index.ts new file mode 100644 index 000000000..ad56fccf6 --- /dev/null +++ b/frontend/src/data/enums/index.ts @@ -0,0 +1,2 @@ +export { StorageKey } from "./local-storage"; +export { Role } from "./role"; diff --git a/frontend/src/data/enums/local-storage.ts b/frontend/src/data/enums/local-storage.ts new file mode 100644 index 000000000..e1b55f9be --- /dev/null +++ b/frontend/src/data/enums/local-storage.ts @@ -0,0 +1,6 @@ +/** + * Keys in LocalStorage + */ +export const enum StorageKey { + BEARER_TOKEN = "bearerToken", +} diff --git a/frontend/src/data/enums/role.ts b/frontend/src/data/enums/role.ts new file mode 100644 index 000000000..e445d0726 --- /dev/null +++ b/frontend/src/data/enums/role.ts @@ -0,0 +1,8 @@ +/** + * Enum for the different levels of authority a user + * can have + */ +export const enum Role { + ADMIN, + COACH, +} diff --git a/frontend/src/utils/local-storage/auth.ts b/frontend/src/utils/local-storage/auth.ts new file mode 100644 index 000000000..908c0ff3a --- /dev/null +++ b/frontend/src/utils/local-storage/auth.ts @@ -0,0 +1,16 @@ +import { StorageKey } from "../../data/enums"; + +/** + * Write the new value of a token into LocalStorage + */ +export function setToken(value: string) { + localStorage.setItem(StorageKey.BEARER_TOKEN, value); +} + +/** + * Pull the user's token out of LocalStorage + * Returns null if there is no token in LocalStorage yet + */ +export function getToken(): string | null { + return localStorage.getItem(StorageKey.BEARER_TOKEN); +} diff --git a/frontend/src/utils/local-storage/index.ts b/frontend/src/utils/local-storage/index.ts new file mode 100644 index 000000000..29a2355c0 --- /dev/null +++ b/frontend/src/utils/local-storage/index.ts @@ -0,0 +1 @@ +export { getToken, setToken } from "./auth"; diff --git a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx new file mode 100644 index 000000000..2b8a32602 --- /dev/null +++ b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx @@ -0,0 +1,15 @@ +import { useContext, useEffect } from "react"; +import { AuthContext } from "../../contexts"; + +export default function VerifyingTokenPage() { + const authContext = useContext(AuthContext); + + useEffect(() => { + const verifyToken = async () => {}; + + verifyToken(); + }, []); + + // This will be replaced later on + return

Loading...

; +} diff --git a/frontend/src/views/VerifyingTokenPage/index.ts b/frontend/src/views/VerifyingTokenPage/index.ts new file mode 100644 index 000000000..12a3d4b87 --- /dev/null +++ b/frontend/src/views/VerifyingTokenPage/index.ts @@ -0,0 +1 @@ +export { default } from "./VerifyingTokenPage"; From d93f3e9dc5b855180f8449067528b94f84f031a3 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 28 Mar 2022 00:23:57 +0200 Subject: [PATCH 176/536] Add route to find current user from token --- backend/src/app/routers/users/users.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index fbc44b7e5..0550da33a 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -3,9 +3,10 @@ from src.app.routers.tags import Tags import src.app.logic.users as logic -from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse -from src.app.utils.dependencies import require_admin +from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, User as UserSchema +from src.app.utils.dependencies import require_admin, get_current_active_user from src.database.database import get_session +from src.database.models import User as UserDB users_router = APIRouter(prefix="/users", tags=[Tags.USERS]) @@ -19,6 +20,12 @@ async def get_users(admin: bool = Query(False), edition: str | None = Query(None return logic.get_users_list(db, admin, edition) +@users_router.get("/current", response_model=UserSchema) +async def get_current_user(user: UserDB = Depends(get_current_active_user)): + """Get a user based on their authorization credentials""" + return user + + @users_router.patch("/{user_id}", status_code=204, dependencies=[Depends(require_admin)]) async def patch_admin_status(user_id: int, admin: AdminPatch, db: Session = Depends(get_session)): """ From cc82526a51f09c810ac84b883e744f1ed2694b87 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 28 Mar 2022 01:06:08 +0200 Subject: [PATCH 177/536] Verify token in frontend --- frontend/src/Router.tsx | 38 +++++++++++-------- frontend/src/data/interfaces/index.ts | 1 + frontend/src/data/interfaces/users.ts | 5 +++ frontend/src/utils/api/api.ts | 15 ++++++++ frontend/src/utils/api/index.ts | 1 + .../src/utils/api/validateRegisterLink.ts | 28 ++++++++++++++ frontend/src/views/LoginPage/LoginPage.tsx | 15 +++++++- .../VerifyingTokenPage/VerifyingTokenPage.tsx | 24 +++++++++++- 8 files changed, 108 insertions(+), 19 deletions(-) create mode 100644 frontend/src/data/interfaces/index.ts create mode 100644 frontend/src/data/interfaces/users.ts diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index cbd7020e7..5104e4486 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -16,28 +16,34 @@ import Footer from "./components/Footer"; export default function Router() { const authContext = useContext(AuthContext); - return authContext.isLoggedIn === null ? ( - // If verification hasn't completed, show nothing - ) : !authContext.isLoggedIn ? ( // If the user isn't logged in, show the login page - - ) : ( + return ( // If the user IS logged in, render the actual app - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - + {authContext.isLoggedIn === null ? ( + // Busy verifying the access token + + ) : !authContext.isLoggedIn ? ( + // User is not logged in -> go to login page + + ) : ( + // Logged in -> show app + + } + /> + } /> + } /> + } /> + } /> + } /> + } /> + + )} -
Your token: {authContext.token}
diff --git a/frontend/src/data/interfaces/index.ts b/frontend/src/data/interfaces/index.ts new file mode 100644 index 000000000..6d7c3694d --- /dev/null +++ b/frontend/src/data/interfaces/index.ts @@ -0,0 +1 @@ +export type { User } from "./users"; diff --git a/frontend/src/data/interfaces/users.ts b/frontend/src/data/interfaces/users.ts new file mode 100644 index 000000000..0d7135e0d --- /dev/null +++ b/frontend/src/data/interfaces/users.ts @@ -0,0 +1,5 @@ +export interface User { + userId: number; + name: string; + admin: boolean; +} diff --git a/frontend/src/utils/api/api.ts b/frontend/src/utils/api/api.ts index 4ee8b6e30..8d5423fdc 100644 --- a/frontend/src/utils/api/api.ts +++ b/frontend/src/utils/api/api.ts @@ -3,3 +3,18 @@ import { BASE_URL } from "../../settings"; export const axiosInstance = axios.create(); axiosInstance.defaults.baseURL = BASE_URL; + +/** + * Set the default bearer token in the request headers + */ +export function setBearerToken(value: string | null) { + // Remove the header + // Note: setting to "null" or "undefined" is not possible + if (value === null) { + delete axiosInstance.defaults.headers.common.Authorization; + + return; + } + + axiosInstance.defaults.headers.common.Authorization = value; +} diff --git a/frontend/src/utils/api/index.ts b/frontend/src/utils/api/index.ts index 064c34216..97302a8d7 100644 --- a/frontend/src/utils/api/index.ts +++ b/frontend/src/utils/api/index.ts @@ -1 +1,2 @@ export { validateRegistrationUrl } from "./validateRegisterLink"; +export { setBearerToken } from "./api"; diff --git a/frontend/src/utils/api/validateRegisterLink.ts b/frontend/src/utils/api/validateRegisterLink.ts index f34d4480c..8526d3b8d 100644 --- a/frontend/src/utils/api/validateRegisterLink.ts +++ b/frontend/src/utils/api/validateRegisterLink.ts @@ -1,5 +1,33 @@ import axios from "axios"; import { axiosInstance } from "./api"; +import { User } from "../../data/interfaces"; + +/** + * Check if a bearer token is valid + * @param token + */ +export async function validateBearerToken(token: string | null): Promise { + // No token stored -> can't validate anything + if (token === null) return null; + + // TODO uncomment once it works + // try { + // const response = await axiosInstance.get("/users/current"); + // return response.data as User; + // } catch (error) { + // if (axios.isAxiosError(error)) { + // return null; + // } else { + // throw error; + // } + // } + + return { + userId: 1, + name: "admin", + admin: true, + }; +} /** * Check if a registration url exists by sending a GET to it, diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index 435460534..efcad1150 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { logIn } from "../../utils/api/login"; @@ -13,12 +13,25 @@ import { NoAccount, LoginButton, } from "./styles"; +import "./LoginPage.css"; +import { AuthContext } from "../../contexts"; function LoginPage({ setToken }: any) { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); + const authContext = useContext(AuthContext); const navigate = useNavigate(); + useEffect(() => { + // If the user is already logged in, redirect them to + // the "students" page instead of showing the login page + if (authContext.isLoggedIn) { + // TODO find other homepage to go to + // editions? + navigate("/students"); + } + }, [navigate, authContext]); + async function callLogIn() { try { const response = await logIn({ setToken }, email, password); diff --git a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx index 2b8a32602..0ef808c5c 100644 --- a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx +++ b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx @@ -1,14 +1,34 @@ import { useContext, useEffect } from "react"; import { AuthContext } from "../../contexts"; +import { setBearerToken } from "../../utils/api"; +import { validateBearerToken } from "../../utils/api/auth"; +import { Role } from "../../data/enums"; + export default function VerifyingTokenPage() { const authContext = useContext(AuthContext); useEffect(() => { - const verifyToken = async () => {}; + const verifyToken = async () => { + const response = await validateBearerToken(authContext.token); + + if (response === null) { + authContext.setToken(null); + authContext.setIsLoggedIn(false); + authContext.setRole(null); + } else { + // Token was valid, use it as the default request header + setBearerToken(authContext.token); + authContext.setIsLoggedIn(true); + authContext.setRole(response.admin ? Role.ADMIN : Role.COACH); + } + }; + // Eslint doesn't like this, but it's the React way verifyToken(); - }, []); + }, [authContext]); + + setBearerToken("test"); // This will be replaced later on return

Loading...

; From 042d5dd754cca1f6137207c8548976044b568e36 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 28 Mar 2022 13:23:51 +0200 Subject: [PATCH 178/536] Create custom hook & more cleanup --- frontend/src/App.tsx | 25 ++------ frontend/src/Router.tsx | 23 +++---- frontend/src/contexts/auth-context.ts | 30 --------- frontend/src/contexts/auth-context.tsx | 62 +++++++++++++++++++ frontend/src/contexts/index.ts | 2 +- frontend/src/views/LoginPage/LoginPage.tsx | 13 ++-- .../VerifyingTokenPage/VerifyingTokenPage.tsx | 6 +- 7 files changed, 87 insertions(+), 74 deletions(-) delete mode 100644 frontend/src/contexts/auth-context.ts create mode 100644 frontend/src/contexts/auth-context.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 83c08b2b0..40512d14d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,32 +1,15 @@ -import React, { useState } from "react"; +import React from "react"; import "bootstrap/dist/css/bootstrap.min.css"; import "./App.css"; -import { AuthContext, AuthContextState } from "./contexts"; import Router from "./Router"; -import { Role } from "./data/enums"; -import { getToken } from "./utils/local-storage"; +import { AuthProvider } from "./contexts"; function App() { - const [isLoggedIn, setIsLoggedIn] = useState(null); - const [role, setRole] = useState(null); - // Default value: check LocalStorage - const [token, setToken] = useState(getToken()); - - // Create AuthContext value - const authContextValue: AuthContextState = { - isLoggedIn: isLoggedIn, - setIsLoggedIn: setIsLoggedIn, - role: role, - setRole: setRole, - token: token, - setToken: setToken, - }; - return ( // AuthContext should be visible in the entire application - + - + ); } diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 5104e4486..d9d483a3b 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -1,5 +1,4 @@ -import React, { useContext } from "react"; -import { AuthContext } from "./contexts"; +import React from "react"; import VerifyingTokenPage from "./views/VerifyingTokenPage"; import LoginPage from "./views/LoginPage"; import { Container, ContentWrapper } from "./app.styles"; @@ -12,29 +11,27 @@ import ProjectsPage from "./views/ProjectsPage/ProjectsPage"; import PendingPage from "./views/PendingPage"; import ErrorPage from "./views/ErrorPage"; import Footer from "./components/Footer"; +import { useAuth } from "./contexts/auth-context"; export default function Router() { - const authContext = useContext(AuthContext); + const { token, setToken, isLoggedIn } = useAuth(); return ( - // If the user IS logged in, render the actual app - + {/* TODO don't pass token & setToken but use useAuth */} + {isLoggedIn && } - {authContext.isLoggedIn === null ? ( + {isLoggedIn === null ? ( // Busy verifying the access token - ) : !authContext.isLoggedIn ? ( + ) : !isLoggedIn ? ( // User is not logged in -> go to login page ) : ( - // Logged in -> show app + // If the user IS logged in, render the actual app - } - /> + } /> } /> } /> } /> @@ -44,7 +41,7 @@ export default function Router() { )} -
+ {isLoggedIn &&
} ); diff --git a/frontend/src/contexts/auth-context.ts b/frontend/src/contexts/auth-context.ts deleted file mode 100644 index d9a55c85a..000000000 --- a/frontend/src/contexts/auth-context.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** Context hook to maintain the authentication state of the user **/ -import { Role } from "../data/enums"; -import React from "react"; -import { getToken } from "../utils/local-storage"; - -export interface AuthContextState { - isLoggedIn: boolean | null; - setIsLoggedIn: (value: boolean | null) => void; - role: Role | null; - setRole: (value: Role | null) => void; - token: string | null; - setToken: (value: string | null) => void; -} - -/** - * Create a placeholder default value for the state - */ -function authDefaultState(): AuthContextState { - return { - isLoggedIn: null, - setIsLoggedIn: (_: boolean | null) => {}, - role: null, - setRole: (_: Role | null) => {}, - token: getToken(), - setToken: (_: string | null) => {}, - }; -} - -// Set isLoggedIn to null on startup while we verify the token of the user -export const AuthContext = React.createContext(authDefaultState()); diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx new file mode 100644 index 000000000..bf28e102d --- /dev/null +++ b/frontend/src/contexts/auth-context.tsx @@ -0,0 +1,62 @@ +/** Context hook to maintain the authentication state of the user **/ +import { Role } from "../data/enums"; +import React, { useContext, ReactNode, useState } from "react"; +import { getToken } from "../utils/local-storage"; + +export interface AuthContextState { + isLoggedIn: boolean | null; + setIsLoggedIn: (value: boolean | null) => void; + role: Role | null; + setRole: (value: Role | null) => void; + token: string | null; + setToken: (value: string | null) => void; +} + +/** + * Create a placeholder default value for the state + */ +function authDefaultState(): AuthContextState { + return { + isLoggedIn: null, + setIsLoggedIn: (_: boolean | null) => {}, + role: null, + setRole: (_: Role | null) => {}, + token: getToken(), + setToken: (_: string | null) => {}, + }; +} + +const AuthContext = React.createContext(authDefaultState()); + +/** + * Custom React hook to use our authentication context + */ +export function useAuth(): AuthContextState { + return useContext(AuthContext); +} + +/** + * Provider for auth that creates getters, setters, maintains state, and + * provides default values + * + * Not strictly necessary but keeps the main App clean by handling this + * code here instead + */ +export function AuthProvider({ children }: { children: ReactNode }) { + const [isLoggedIn, setIsLoggedIn] = useState(null); + const [role, setRole] = useState(null); + // Default value: check LocalStorage + const [token, setToken] = useState(getToken()); + + // Create AuthContext value + const authContextValue: AuthContextState = { + isLoggedIn: isLoggedIn, + setIsLoggedIn: setIsLoggedIn, + role: role, + setRole: setRole, + token: token, + setToken: setToken, + }; + + return {children}; +} diff --git a/frontend/src/contexts/index.ts b/frontend/src/contexts/index.ts index 358311d76..067ec1341 100644 --- a/frontend/src/contexts/index.ts +++ b/frontend/src/contexts/index.ts @@ -1,3 +1,3 @@ import type { AuthContextState } from "./auth-context"; export type { AuthContextState }; -export { AuthContext } from "./auth-context"; +export { AuthProvider } from "./auth-context"; diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index efcad1150..2c692aefe 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { logIn } from "../../utils/api/login"; @@ -14,23 +14,24 @@ import { LoginButton, } from "./styles"; import "./LoginPage.css"; -import { AuthContext } from "../../contexts"; +import { useAuth } from "../../contexts/auth-context"; function LoginPage({ setToken }: any) { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); - const authContext = useContext(AuthContext); + const { isLoggedIn } = useAuth(); const navigate = useNavigate(); useEffect(() => { // If the user is already logged in, redirect them to // the "students" page instead of showing the login page - if (authContext.isLoggedIn) { + if (isLoggedIn) { // TODO find other homepage to go to - // editions? + // perhaps editions? + // (the rest requires an edition) navigate("/students"); } - }, [navigate, authContext]); + }, [navigate, isLoggedIn]); async function callLogIn() { try { diff --git a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx index 0ef808c5c..3dbce8811 100644 --- a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx +++ b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx @@ -1,12 +1,12 @@ -import { useContext, useEffect } from "react"; -import { AuthContext } from "../../contexts"; +import { useEffect } from "react"; import { setBearerToken } from "../../utils/api"; import { validateBearerToken } from "../../utils/api/auth"; import { Role } from "../../data/enums"; +import { useAuth } from "../../contexts/auth-context"; export default function VerifyingTokenPage() { - const authContext = useContext(AuthContext); + const authContext = useAuth(); useEffect(() => { const verifyToken = async () => { From 18b97f6f289c9a2eeabd741faf4a62c884ab1cee Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 28 Mar 2022 14:55:54 +0200 Subject: [PATCH 179/536] Update login route to return user data --- backend/src/app/logic/users.py | 6 ++++ backend/src/app/routers/login/login.py | 7 ++-- backend/src/app/schemas/login.py | 22 +++++++++---- backend/src/database/crud/users.py | 6 ++++ .../test_database/test_crud/test_users.py | 33 +++++++++++++++++++ 5 files changed, 66 insertions(+), 8 deletions(-) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index 5a10f6e70..aa9b1743c 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -2,6 +2,7 @@ from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, UserRequest import src.database.crud.users as users_crud +from src.database.models import User def get_users_list(db: Session, admin: bool, edition_name: str | None) -> UsersListResponse: @@ -24,6 +25,11 @@ def get_users_list(db: Session, admin: bool, edition_name: str | None) -> UsersL return UsersListResponse(users=users_orm) +def get_user_editions(user: User) -> list[int]: + """Get all id's of the editions this user is coach in""" + return users_crud.get_user_edition_ids(user) + + def edit_admin_status(db: Session, user_id: int, admin: AdminPatch): """ Edit the admin-status of a user diff --git a/backend/src/app/routers/login/login.py b/backend/src/app/routers/login/login.py index c46099c17..e6640e696 100644 --- a/backend/src/app/routers/login/login.py +++ b/backend/src/app/routers/login/login.py @@ -8,9 +8,11 @@ import settings from src.app.exceptions.authentication import InvalidCredentialsException +from src.app.logic.users import get_user_editions +from src.app.logic.security import authenticate_user, ACCESS_TOKEN_EXPIRE_HOURS, create_access_token from src.app.logic.security import authenticate_user, create_access_token from src.app.routers.tags import Tags -from src.app.schemas.login import Token +from src.app.schemas.login import Token, UserData from src.database.database import get_session login_router = APIRouter(prefix="/login", tags=[Tags.LOGIN]) @@ -32,4 +34,5 @@ async def login_for_access_token(db: Session = Depends(get_session), data={"sub": str(user.user_id)}, expires_delta=access_token_expires ) - return {"access_token": access_token, "token_type": "bearer"} + user_data = UserData(admin=user.admin, editions=get_user_editions(user)) + return {"access_token": access_token, "token_type": "bearer", "user": user_data} diff --git a/backend/src/app/schemas/login.py b/backend/src/app/schemas/login.py index c20799879..916daf416 100644 --- a/backend/src/app/schemas/login.py +++ b/backend/src/app/schemas/login.py @@ -1,12 +1,22 @@ from src.app.schemas.utils import CamelCaseModel +class UserData(CamelCaseModel): + """User information that can be passed to frontend + Includes the id's of the editions a user is coach in + TODO replace with names once id-name change is merged + """ + admin: bool + editions: list[int] = [] + + class Config: + orm_mode = True + + class Token(CamelCaseModel): - """Token generated after login""" + """Token generated after login + Also contains data about the User to set permissions in frontend + """ access_token: str token_type: str - - -class User(CamelCaseModel): - """The fields used to find a user in the DB""" - user_id: int + user: UserData diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index f89a01b7b..3ae256b62 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -18,6 +18,12 @@ def get_all_users(db: Session) -> list[User]: return db.query(User).all() +def get_user_edition_ids(user: User) -> list[int]: + """Get all id's of the editions this user is coach in""" + # TODO replace with name + return list(map(lambda e: e.edition_id, user.editions)) + + def get_users_from_edition(db: Session, edition_name: str) -> list[User]: """ Get all coaches from the given edition diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index 549000694..293da9f2f 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -59,6 +59,39 @@ def test_get_all_admins(database_session: Session, data: dict[str, str]): assert data["user1"] == users[0].user_id +def test_get_user_edition_ids_empty(database_session: Session): + """Test getting all editions from a user when there are none""" + user = models.User(name="test", email="test@email.com") + database_session.add(user) + database_session.commit() + + # No editions yet + editions = users_crud.get_user_edition_ids(user) + assert len(editions) == 0 + + +def test_get_user_edition_ids(database_session: Session): + """Test getting all editions from a user when they aren't empty""" + user = models.User(name="test", email="test@email.com") + database_session.add(user) + database_session.commit() + + # No editions yet + editions = users_crud.get_user_edition_ids(user) + assert len(editions) == 0 + + # Add user to a new edition + edition = models.Edition(year=2022) + user.editions.append(edition) + database_session.add(edition) + database_session.add(user) + database_session.commit() + + # No editions yet + editions = users_crud.get_user_edition_ids(user) + assert editions == [edition.edition_id] + + def test_get_all_users_from_edition(database_session: Session, data: dict[str, str]): """Test get request for users of a given edition""" From 7fd0a4ef720ae63e54e20222d223ce2789a49db8 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 28 Mar 2022 17:27:00 +0200 Subject: [PATCH 180/536] Create private & admin routes --- frontend/src/Router.tsx | 27 ++++++++++++------- .../src/components/AdminRoute/AdminRoute.tsx | 19 +++++++++++++ frontend/src/components/AdminRoute/index.ts | 1 + .../components/PrivateRoute/PrivateRoute.tsx | 11 ++++++++ frontend/src/components/PrivateRoute/index.ts | 1 + frontend/src/contexts/auth-context.tsx | 18 +++++++++++-- frontend/src/data/interfaces/users.ts | 1 + frontend/src/utils/api/api.ts | 1 - frontend/src/utils/api/login.ts | 16 +++++++++-- .../src/utils/api/validateRegisterLink.ts | 3 ++- frontend/src/views/ErrorPage/ErrorPage.tsx | 12 --------- frontend/src/views/ErrorPage/index.ts | 1 - frontend/src/views/ErrorPage/styles.ts | 8 ------ frontend/src/views/LoginPage/LoginPage.tsx | 10 +++---- .../errors/ForbiddenPage/ForbiddenPage.tsx | 12 +++++++++ .../src/views/errors/ForbiddenPage/index.ts | 1 + .../errors/NotFoundPage/NotFoundPage.css | 0 .../errors/NotFoundPage/NotFoundPage.tsx | 12 +++++++++ .../src/views/errors/NotFoundPage/index.ts | 1 + .../src/views/errors/NotFoundPage/styles.ts | 0 frontend/src/views/errors/index.ts | 1 + frontend/src/views/errors/styles.ts | 8 ++++++ 22 files changed, 122 insertions(+), 42 deletions(-) create mode 100644 frontend/src/components/AdminRoute/AdminRoute.tsx create mode 100644 frontend/src/components/AdminRoute/index.ts create mode 100644 frontend/src/components/PrivateRoute/PrivateRoute.tsx create mode 100644 frontend/src/components/PrivateRoute/index.ts delete mode 100644 frontend/src/views/ErrorPage/index.ts delete mode 100644 frontend/src/views/ErrorPage/styles.ts create mode 100644 frontend/src/views/errors/ForbiddenPage/ForbiddenPage.tsx create mode 100644 frontend/src/views/errors/ForbiddenPage/index.ts create mode 100644 frontend/src/views/errors/NotFoundPage/NotFoundPage.css create mode 100644 frontend/src/views/errors/NotFoundPage/NotFoundPage.tsx create mode 100644 frontend/src/views/errors/NotFoundPage/index.ts create mode 100644 frontend/src/views/errors/NotFoundPage/styles.ts create mode 100644 frontend/src/views/errors/index.ts create mode 100644 frontend/src/views/errors/styles.ts diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index d9d483a3b..4ceb0df5a 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -9,9 +9,12 @@ import StudentsPage from "./views/StudentsPage/StudentsPage"; import UsersPage from "./views/UsersPage"; import ProjectsPage from "./views/ProjectsPage/ProjectsPage"; import PendingPage from "./views/PendingPage"; -import ErrorPage from "./views/ErrorPage"; import Footer from "./components/Footer"; import { useAuth } from "./contexts/auth-context"; +import PrivateRoute from "./components/PrivateRoute"; +import AdminRoute from "./components/AdminRoute"; +import { NotFoundPage } from "./views/errors"; +import ForbiddenPage from "./views/errors/ForbiddenPage"; export default function Router() { const { token, setToken, isLoggedIn } = useAuth(); @@ -25,19 +28,23 @@ export default function Router() { {isLoggedIn === null ? ( // Busy verifying the access token - ) : !isLoggedIn ? ( - // User is not logged in -> go to login page - ) : ( - // If the user IS logged in, render the actual app + // Access token was checked, if it is invalid + // then the will redirect to + // the LoginPage - } /> - } /> - } /> - } /> + } /> + } /> + }> + } /> + + }> + } /> + } /> } /> - } /> + } /> + } /> )} diff --git a/frontend/src/components/AdminRoute/AdminRoute.tsx b/frontend/src/components/AdminRoute/AdminRoute.tsx new file mode 100644 index 000000000..0e027003d --- /dev/null +++ b/frontend/src/components/AdminRoute/AdminRoute.tsx @@ -0,0 +1,19 @@ +import { Navigate, Outlet } from "react-router-dom"; +import { useAuth } from "../../contexts/auth-context"; +import { Role } from "../../data/enums"; + +/** + * React component for admin-only routes + * Goes to login page if not authenticated, and to 403 + * if not admin + */ +export default function AdminRoute() { + const { isLoggedIn, role } = useAuth(); + return !isLoggedIn ? ( + + ) : role === Role.COACH ? ( + + ) : ( + + ); +} diff --git a/frontend/src/components/AdminRoute/index.ts b/frontend/src/components/AdminRoute/index.ts new file mode 100644 index 000000000..7d4693844 --- /dev/null +++ b/frontend/src/components/AdminRoute/index.ts @@ -0,0 +1 @@ +export { default } from "./AdminRoute"; diff --git a/frontend/src/components/PrivateRoute/PrivateRoute.tsx b/frontend/src/components/PrivateRoute/PrivateRoute.tsx new file mode 100644 index 000000000..4972a39ad --- /dev/null +++ b/frontend/src/components/PrivateRoute/PrivateRoute.tsx @@ -0,0 +1,11 @@ +import { useAuth } from "../../contexts/auth-context"; +import { Navigate, Outlet } from "react-router-dom"; + +/** + * React component that goes to the login page if not authenticated + */ +export default function PrivateRoute() { + const { isLoggedIn } = useAuth(); + // TODO check edition once routes have been moved over + return isLoggedIn ? : ; +} diff --git a/frontend/src/components/PrivateRoute/index.ts b/frontend/src/components/PrivateRoute/index.ts new file mode 100644 index 000000000..9b61d0e92 --- /dev/null +++ b/frontend/src/components/PrivateRoute/index.ts @@ -0,0 +1 @@ +export { default } from "./PrivateRoute"; diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx index bf28e102d..521a90b60 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 { getToken } from "../utils/local-storage"; +import { getToken, setToken as setTokenInStorage } from "../utils/local-storage"; export interface AuthContextState { isLoggedIn: boolean | null; @@ -10,6 +10,8 @@ export interface AuthContextState { setRole: (value: Role | null) => void; token: string | null; setToken: (value: string | null) => void; + editions: number[]; + setEditions: (value: number[]) => void; } /** @@ -23,6 +25,8 @@ function authDefaultState(): AuthContextState { setRole: (_: Role | null) => {}, token: getToken(), setToken: (_: string | null) => {}, + editions: [], + setEditions: (_: number[]) => {}, }; } @@ -45,6 +49,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([]); // Default value: check LocalStorage const [token, setToken] = useState(getToken()); @@ -55,7 +60,16 @@ export function AuthProvider({ children }: { children: ReactNode }) { role: role, setRole: setRole, token: token, - setToken: setToken, + setToken: (value: string | null) => { + // Set the token in LocalStorage + if (value) { + setTokenInStorage(value); + } + + setToken(value); + }, + editions: editions, + setEditions: setEditions, }; return {children}; diff --git a/frontend/src/data/interfaces/users.ts b/frontend/src/data/interfaces/users.ts index 0d7135e0d..04114d5a2 100644 --- a/frontend/src/data/interfaces/users.ts +++ b/frontend/src/data/interfaces/users.ts @@ -2,4 +2,5 @@ export interface User { userId: number; name: string; admin: boolean; + editions: number[]; } diff --git a/frontend/src/utils/api/api.ts b/frontend/src/utils/api/api.ts index 8d5423fdc..e88e6b3d2 100644 --- a/frontend/src/utils/api/api.ts +++ b/frontend/src/utils/api/api.ts @@ -12,7 +12,6 @@ export function setBearerToken(value: string | null) { // Note: setting to "null" or "undefined" is not possible if (value === null) { delete axiosInstance.defaults.headers.common.Authorization; - return; } diff --git a/frontend/src/utils/api/login.ts b/frontend/src/utils/api/login.ts index b4427b8ae..4fede8e34 100644 --- a/frontend/src/utils/api/login.ts +++ b/frontend/src/utils/api/login.ts @@ -1,23 +1,35 @@ import axios from "axios"; import { axiosInstance } from "./api"; +import { AuthContextState } from "../../contexts"; +import { Role } from "../../data/enums"; interface LoginResponse { accessToken: string; + user: { + admin: boolean; + editions: number[]; + }; } -export async function logIn({ setToken }: any, email: string, password: string) { +export async function logIn(auth: AuthContextState, email: string, password: string) { const payload = new FormData(); payload.append("username", email); payload.append("password", password); + try { const response = await axiosInstance.post("/login/token", payload); const login = response.data as LoginResponse; - await setToken(login.accessToken); + auth.setToken(login.accessToken); + auth.setIsLoggedIn(true); + auth.setRole(login.user.admin ? Role.ADMIN : Role.COACH); + return true; } catch (error) { if (axios.isAxiosError(error)) { + auth.setIsLoggedIn(false); return false; } else { + auth.setIsLoggedIn(null); throw error; } } diff --git a/frontend/src/utils/api/validateRegisterLink.ts b/frontend/src/utils/api/validateRegisterLink.ts index 8526d3b8d..beea2e4b3 100644 --- a/frontend/src/utils/api/validateRegisterLink.ts +++ b/frontend/src/utils/api/validateRegisterLink.ts @@ -25,7 +25,8 @@ export async function validateBearerToken(token: string | null): Promise - Oops! This is awkward... You are looking for something that doesn't actually exist. - - ); -} - -export default ErrorPage; diff --git a/frontend/src/views/ErrorPage/index.ts b/frontend/src/views/ErrorPage/index.ts deleted file mode 100644 index 4a1ddb1bf..000000000 --- a/frontend/src/views/ErrorPage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./ErrorPage"; diff --git a/frontend/src/views/ErrorPage/styles.ts b/frontend/src/views/ErrorPage/styles.ts deleted file mode 100644 index 23e2ce164..000000000 --- a/frontend/src/views/ErrorPage/styles.ts +++ /dev/null @@ -1,8 +0,0 @@ -import styled from "styled-components"; - -export const ErrorMessage = styled.h1` - margin: auto; - margin-top: 10%; - max-width: 50%; - text-align: center; -`; diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index 2c692aefe..5a9f0ade5 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -16,26 +16,26 @@ import { import "./LoginPage.css"; import { useAuth } from "../../contexts/auth-context"; -function LoginPage({ setToken }: any) { +function LoginPage() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); - const { isLoggedIn } = useAuth(); + const authCtx = useAuth(); const navigate = useNavigate(); useEffect(() => { // If the user is already logged in, redirect them to // the "students" page instead of showing the login page - if (isLoggedIn) { + if (authCtx.isLoggedIn) { // TODO find other homepage to go to // perhaps editions? // (the rest requires an edition) navigate("/students"); } - }, [navigate, isLoggedIn]); + }, [authCtx.isLoggedIn, navigate]); async function callLogIn() { try { - const response = await logIn({ setToken }, email, password); + const response = await logIn(authCtx, email, password); if (response) navigate("/students"); else alert("Something went wrong when login in"); } catch (error) { diff --git a/frontend/src/views/errors/ForbiddenPage/ForbiddenPage.tsx b/frontend/src/views/errors/ForbiddenPage/ForbiddenPage.tsx new file mode 100644 index 000000000..b9183c26e --- /dev/null +++ b/frontend/src/views/errors/ForbiddenPage/ForbiddenPage.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { ErrorContainer } from "../styles"; + +function ForbiddenPage() { + return ( + +

You don't have access to that page.

+
+ ); +} + +export default ForbiddenPage; diff --git a/frontend/src/views/errors/ForbiddenPage/index.ts b/frontend/src/views/errors/ForbiddenPage/index.ts new file mode 100644 index 000000000..fdbe26d7a --- /dev/null +++ b/frontend/src/views/errors/ForbiddenPage/index.ts @@ -0,0 +1 @@ +export { default } from "./ForbiddenPage"; diff --git a/frontend/src/views/errors/NotFoundPage/NotFoundPage.css b/frontend/src/views/errors/NotFoundPage/NotFoundPage.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/views/errors/NotFoundPage/NotFoundPage.tsx b/frontend/src/views/errors/NotFoundPage/NotFoundPage.tsx new file mode 100644 index 000000000..e2b5b035d --- /dev/null +++ b/frontend/src/views/errors/NotFoundPage/NotFoundPage.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import "./NotFoundPage.css"; + +function NotFoundPage() { + return ( +

+ Oops! This is awkward... You are looking for something that doesn't exist. +

+ ); +} + +export default NotFoundPage; diff --git a/frontend/src/views/errors/NotFoundPage/index.ts b/frontend/src/views/errors/NotFoundPage/index.ts new file mode 100644 index 000000000..225a37766 --- /dev/null +++ b/frontend/src/views/errors/NotFoundPage/index.ts @@ -0,0 +1 @@ +export { default } from "./NotFoundPage"; diff --git a/frontend/src/views/errors/NotFoundPage/styles.ts b/frontend/src/views/errors/NotFoundPage/styles.ts new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/views/errors/index.ts b/frontend/src/views/errors/index.ts new file mode 100644 index 000000000..67c80d24a --- /dev/null +++ b/frontend/src/views/errors/index.ts @@ -0,0 +1 @@ +export { default as NotFoundPage } from "./NotFoundPage"; diff --git a/frontend/src/views/errors/styles.ts b/frontend/src/views/errors/styles.ts new file mode 100644 index 000000000..f7ae5a7e3 --- /dev/null +++ b/frontend/src/views/errors/styles.ts @@ -0,0 +1,8 @@ +import styled from "styled-components"; + +export const ErrorContainer = styled.div` + text-align: center; + display: flex; + justify-content: center; + align-items: center; +`; From e0598d02a5b7c81ec74d3f762c8be6d818b75d78 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 28 Mar 2022 18:09:55 +0200 Subject: [PATCH 181/536] Fix failing test --- frontend/package.json | 4 + frontend/src/App.test.tsx | 10 +- frontend/src/setupTests.ts | 20 ++ .../api/{validateRegisterLink.ts => auth.ts} | 0 frontend/src/utils/api/index.ts | 2 +- frontend/yarn.lock | 324 +++++++++++++++++- 6 files changed, 338 insertions(+), 22 deletions(-) rename frontend/src/utils/api/{validateRegisterLink.ts => auth.ts} (100%) diff --git a/frontend/package.json b/frontend/package.json index b5a0d7f18..57b1ecc33 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,8 @@ "@testing-library/react": "^12.0.0", "@testing-library/user-event": "^13.2.1", "@types/axios": "^0.14.0", + "@types/enzyme": "^3.10.11", + "@types/enzyme-adapter-react-16": "^1.0.6", "@types/jest": "^27.0.1", "@types/node": "^16.7.13", "@types/react": "^17.0.20", @@ -34,6 +36,8 @@ "@types/styled-components": "^5.1.24", "@typescript-eslint/eslint-plugin": "^5.12.0", "@typescript-eslint/parser": "^5.12.0", + "enzyme": "^3.11.0", + "enzyme-adapter-react-16": "^1.15.6", "eslint": "^8.9.0", "eslint-config-prettier": "^8.3.0", "eslint-config-standard": "^16.0.3", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index f92c1e6bc..ca04e7e7e 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -1,9 +1,9 @@ import React from "react"; -import { render, screen } from "@testing-library/react"; +import { shallow } from "enzyme"; import App from "./App"; +import Router from "./Router"; -test("renders Open Summer Of Code", () => { - render(); - const linkElement = screen.getByText(/Welcome/i); - expect(linkElement).toBeInTheDocument(); +test("app contains router", () => { + const page = shallow(); + expect(page.find(Router).exists()).toBeTruthy(); }); diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts index 1dd407a63..e4fe2045d 100644 --- a/frontend/src/setupTests.ts +++ b/frontend/src/setupTests.ts @@ -3,3 +3,23 @@ // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import "@testing-library/jest-dom"; +import { configure } from "enzyme"; +import Adapter from "enzyme-adapter-react-16"; + +// Configure Enzyme adapter +configure({ adapter: new Adapter() }); + +// Mock Axios so the tests never make API calls +jest.mock("axios", () => { + return { + create: () => { + return { + defaults: { + baseURL: "", + }, + get: jest.fn(), + post: jest.fn(), + }; + }, + }; +}); diff --git a/frontend/src/utils/api/validateRegisterLink.ts b/frontend/src/utils/api/auth.ts similarity index 100% rename from frontend/src/utils/api/validateRegisterLink.ts rename to frontend/src/utils/api/auth.ts diff --git a/frontend/src/utils/api/index.ts b/frontend/src/utils/api/index.ts index 97302a8d7..ef93639c0 100644 --- a/frontend/src/utils/api/index.ts +++ b/frontend/src/utils/api/index.ts @@ -1,2 +1,2 @@ -export { validateRegistrationUrl } from "./validateRegisterLink"; +export { validateRegistrationUrl } from "./auth"; export { setBearerToken } from "./api"; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 4bf18ad1c..9cb056679 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1773,6 +1773,13 @@ dependencies: "@types/node" "*" +"@types/cheerio@*": + version "0.22.31" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.31.tgz#b8538100653d6bb1b08a1e46dec75b4f2a5d5eb6" + 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" @@ -1788,6 +1795,21 @@ 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" + 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== + 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" @@ -2398,6 +2420,21 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.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" + integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg== + dependencies: + array.prototype.find "^2.1.1" + function.prototype.name "^1.1.2" + is-regex "^1.1.0" + object-is "^1.1.2" + object.assign "^4.1.0" + object.entries "^1.1.2" + prop-types "^15.7.2" + prop-types-exact "^1.2.0" + react-is "^16.13.1" + ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -2542,7 +2579,27 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array.prototype.flat@^1.2.5: +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" + integrity sha512-Dk3Ty7N42Odk7PjU/Ci3zT4pLj20YvuVnneG/58ICM6bt4Ij5kZaJTVQ9TSaWaIECX2sFyz4KItkVZqHNnciqw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + es-array-method-boxes-properly "^1.0.0" + is-string "^1.0.7" + +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" + integrity sha512-00S1O4ewO95OmmJW7EesWfQlrCrLEL8kZ40w3+GkLX2yTt0m2ggcePPa2uHPJ9KUmJvwRq+lCV9bD8Yim23x/Q== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + +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" integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg== @@ -3027,6 +3084,30 @@ check-types@^11.1.1: resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f" 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" + integrity sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg== + dependencies: + css-select "^4.1.3" + css-what "^5.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + domutils "^2.7.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" + integrity sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw== + dependencies: + cheerio-select "^1.5.0" + dom-serializer "^1.3.2" + domhandler "^4.2.0" + htmlparser2 "^6.1.0" + parse5 "^6.0.1" + parse5-htmlparser2-tree-adapter "^6.0.1" + tslib "^2.2.0" + chokidar@^3.4.2, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -3143,7 +3224,7 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@^2.20.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" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -3409,7 +3490,7 @@ css-what@^3.2.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== -css-what@^5.1.0: +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" integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== @@ -3690,6 +3771,11 @@ dir-glob@^3.0.1: 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" + integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo= + dlv@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" @@ -3757,7 +3843,7 @@ dom-serializer@0: domelementtype "^2.0.1" entities "^2.0.0" -dom-serializer@^1.0.1: +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" integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== @@ -3798,7 +3884,7 @@ domutils@^1.7.0: dom-serializer "0" domelementtype "1" -domutils@^2.5.2, domutils@^2.8.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" integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== @@ -3885,6 +3971,70 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" 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" + integrity sha512-yFlVJCXh8T+mcQo8M6my9sPgeGzj85HSHi6Apgf1Cvq/7EL/J9+1JoJmJsRxZgyTvPMAqOEpRSu/Ii/ZpyOk0g== + dependencies: + enzyme-adapter-utils "^1.14.0" + enzyme-shallow-equal "^1.0.4" + has "^1.0.3" + object.assign "^4.1.2" + object.values "^1.1.2" + prop-types "^15.7.2" + react-is "^16.13.1" + react-test-renderer "^16.0.0-0" + semver "^5.7.0" + +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" + integrity sha512-F/z/7SeLt+reKFcb7597IThpDp0bmzcH1E9Oabqv+o01cID2/YInlqHbFl7HzWBl4h3OdZYedtwNDOmSKkk0bg== + dependencies: + airbnb-prop-types "^2.16.0" + function.prototype.name "^1.1.3" + has "^1.0.3" + object.assign "^4.1.2" + object.fromentries "^2.0.3" + prop-types "^15.7.2" + semver "^5.7.1" + +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" + integrity sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q== + dependencies: + has "^1.0.3" + object-is "^1.1.2" + +enzyme@^3.11.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.11.0.tgz#71d680c580fe9349f6f5ac6c775bc3e6b7a79c28" + integrity sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw== + dependencies: + array.prototype.flat "^1.2.3" + cheerio "^1.0.0-rc.3" + enzyme-shallow-equal "^1.0.1" + function.prototype.name "^1.1.2" + has "^1.0.3" + html-element-map "^1.2.0" + is-boolean-object "^1.0.1" + is-callable "^1.1.5" + is-number-object "^1.0.4" + is-regex "^1.0.5" + is-string "^1.0.5" + is-subset "^0.1.1" + lodash.escape "^4.0.1" + lodash.isequal "^4.5.0" + object-inspect "^1.7.0" + object-is "^1.0.2" + object.assign "^4.1.0" + object.entries "^1.1.1" + object.values "^1.1.1" + raf "^3.4.1" + rst-selector-parser "^2.2.3" + string.prototype.trim "^1.2.1" + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -3925,6 +4075,11 @@ es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.19.1: string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.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" + 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" @@ -4607,11 +4762,26 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 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" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + 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" 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" + 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" @@ -4815,6 +4985,14 @@ hpack.js@^2.1.6: readable-stream "^2.0.1" wbuf "^1.1.0" +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" + integrity sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg== + dependencies: + array.prototype.filter "^1.0.0" + call-bind "^1.0.2" + 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" @@ -5087,7 +5265,7 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-boolean-object@^1.1.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" integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== @@ -5095,7 +5273,7 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-callable@^1.1.4, is-callable@^1.2.4: +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" integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== @@ -5188,7 +5366,7 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== -is-regex@^1.0.4, is-regex@^1.1.4: +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" integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== @@ -5223,6 +5401,11 @@ is-string@^1.0.5, is-string@^1.0.7: 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" + 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" @@ -5988,6 +6171,21 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" 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" + 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" + 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" + 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" @@ -6175,6 +6373,11 @@ mkdirp@^0.5.5, mkdirp@~0.5.1: 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" + 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" @@ -6213,6 +6416,16 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +nearley@^2.7.10: + version "2.20.1" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474" + integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ== + dependencies: + commander "^2.19.0" + moo "^0.5.0" + railroad-diagrams "^1.0.0" + randexp "0.4.6" + negotiator@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" @@ -6297,12 +6510,12 @@ object-hash@^2.2.0: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== -object-inspect@^1.11.0, object-inspect@^1.9.0: +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" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== -object-is@^1.0.1: +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" integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== @@ -6325,7 +6538,7 @@ object.assign@^4.1.0, object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" -object.entries@^1.1.5: +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" integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== @@ -6334,7 +6547,7 @@ object.entries@^1.1.5: define-properties "^1.1.3" es-abstract "^1.19.1" -object.fromentries@^2.0.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" integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw== @@ -6360,7 +6573,7 @@ object.hasown@^1.1.0: define-properties "^1.1.3" es-abstract "^1.19.1" -object.values@^1.1.0, object.values@^1.1.5: +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" integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== @@ -6532,7 +6745,14 @@ parse-json@^5.0.0, parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse5@6.0.1: +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" + 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" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== @@ -7228,6 +7448,15 @@ prompts@^2.0.1, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" +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" + integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA== + dependencies: + has "^1.0.3" + object.assign "^4.1.0" + reflect.ownkeys "^0.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" @@ -7290,6 +7519,19 @@ raf@^3.4.1: 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" + integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234= + +randexp@0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" + integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== + dependencies: + discontinuous-range "1.0.0" + ret "~0.1.10" + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -7395,7 +7637,7 @@ react-icons@^4.3.1: resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.3.1.tgz#2fa92aebbbc71f43d2db2ed1aed07361124e91ca" integrity sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ== -react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0: +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" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -7497,6 +7739,16 @@ react-social-login-buttons@^3.6.0: resolved "https://registry.yarnpkg.com/react-social-login-buttons/-/react-social-login-buttons-3.6.0.tgz#2be1cb114d8c0200581ba1c8ec5ea74e89cf7701" 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" + integrity sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg== + dependencies: + object-assign "^4.1.1" + prop-types "^15.6.2" + react-is "^16.8.6" + scheduler "^0.19.1" + 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" @@ -7559,6 +7811,11 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +reflect.ownkeys@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" + 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" @@ -7706,6 +7963,11 @@ resolve@^2.0.0-next.3: is-core-module "^2.2.0" path-parse "^1.0.6" +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + 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" @@ -7740,6 +8002,14 @@ rollup@^2.43.1: 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" + integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE= + dependencies: + lodash.flattendeep "^4.4.0" + nearley "^2.7.10" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -7787,6 +8057,14 @@ saxes@^5.0.1: 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" + integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" @@ -7849,6 +8127,11 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" 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" + 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" @@ -8143,6 +8426,15 @@ string.prototype.matchall@^4.0.6: regexp.prototype.flags "^1.4.1" side-channel "^1.0.4" +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" + integrity sha512-Lnh17webJVsD6ECeovpVN17RlAKjmz4rF9S+8Y45CkMc/ufVpTkU3vZIyIC7sllQ1FCvObZnnCdNs/HXTUOTlg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.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" @@ -8508,7 +8800,7 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.3: +tslib@^2.0.3, tslib@^2.2.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== From 110ae19464b645cd4bdcf35fddea7919bef2986c Mon Sep 17 00:00:00 2001 From: stijndcl Date: Thu, 31 Mar 2022 13:31:55 +0200 Subject: [PATCH 182/536] Fix artifacts of merge conflicts --- frontend/src/views/ErrorPage/ErrorPage.tsx | 0 frontend/src/views/LoginPage/LoginPage.tsx | 1 - .../src/views/errors/ForbiddenPage/ForbiddenPage.tsx | 3 ++- frontend/src/views/errors/NotFoundPage/NotFoundPage.css | 0 frontend/src/views/errors/NotFoundPage/NotFoundPage.tsx | 9 +++++---- frontend/src/views/errors/NotFoundPage/styles.ts | 0 frontend/src/views/errors/styles.ts | 1 + 7 files changed, 8 insertions(+), 6 deletions(-) delete mode 100644 frontend/src/views/ErrorPage/ErrorPage.tsx delete mode 100644 frontend/src/views/errors/NotFoundPage/NotFoundPage.css delete mode 100644 frontend/src/views/errors/NotFoundPage/styles.ts diff --git a/frontend/src/views/ErrorPage/ErrorPage.tsx b/frontend/src/views/ErrorPage/ErrorPage.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index 5a9f0ade5..3488b1a67 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -13,7 +13,6 @@ import { NoAccount, LoginButton, } from "./styles"; -import "./LoginPage.css"; import { useAuth } from "../../contexts/auth-context"; function LoginPage() { diff --git a/frontend/src/views/errors/ForbiddenPage/ForbiddenPage.tsx b/frontend/src/views/errors/ForbiddenPage/ForbiddenPage.tsx index b9183c26e..9eced5faf 100644 --- a/frontend/src/views/errors/ForbiddenPage/ForbiddenPage.tsx +++ b/frontend/src/views/errors/ForbiddenPage/ForbiddenPage.tsx @@ -4,7 +4,8 @@ import { ErrorContainer } from "../styles"; function ForbiddenPage() { return ( -

You don't have access to that page.

+

Stop right there!

+

You don't have access to that page.

); } diff --git a/frontend/src/views/errors/NotFoundPage/NotFoundPage.css b/frontend/src/views/errors/NotFoundPage/NotFoundPage.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/src/views/errors/NotFoundPage/NotFoundPage.tsx b/frontend/src/views/errors/NotFoundPage/NotFoundPage.tsx index e2b5b035d..d47a432ae 100644 --- a/frontend/src/views/errors/NotFoundPage/NotFoundPage.tsx +++ b/frontend/src/views/errors/NotFoundPage/NotFoundPage.tsx @@ -1,11 +1,12 @@ import React from "react"; -import "./NotFoundPage.css"; +import { ErrorContainer } from "../styles"; function NotFoundPage() { return ( -

- Oops! This is awkward... You are looking for something that doesn't exist. -

+ +

Oops! This is awkward...

+

You are looking for something that doesn't exist.

+
); } diff --git a/frontend/src/views/errors/NotFoundPage/styles.ts b/frontend/src/views/errors/NotFoundPage/styles.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/src/views/errors/styles.ts b/frontend/src/views/errors/styles.ts index f7ae5a7e3..9242e5ced 100644 --- a/frontend/src/views/errors/styles.ts +++ b/frontend/src/views/errors/styles.ts @@ -5,4 +5,5 @@ export const ErrorContainer = styled.div` display: flex; justify-content: center; align-items: center; + flex-direction: column; `; From 7044f9d6bd3adc6428e6e8f1825553cb84108228 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sat, 2 Apr 2022 15:28:26 +0200 Subject: [PATCH 183/536] Fix broken stuff --- backend/src/app/routers/login/login.py | 1 - backend/tests/test_database/test_crud/test_users.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/src/app/routers/login/login.py b/backend/src/app/routers/login/login.py index e6640e696..d89cb03db 100644 --- a/backend/src/app/routers/login/login.py +++ b/backend/src/app/routers/login/login.py @@ -9,7 +9,6 @@ import settings from src.app.exceptions.authentication import InvalidCredentialsException from src.app.logic.users import get_user_editions -from src.app.logic.security import authenticate_user, ACCESS_TOKEN_EXPIRE_HOURS, create_access_token from src.app.logic.security import authenticate_user, create_access_token from src.app.routers.tags import Tags from src.app.schemas.login import Token, UserData diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index 293da9f2f..d302bb92b 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -61,7 +61,7 @@ def test_get_all_admins(database_session: Session, data: dict[str, str]): def test_get_user_edition_ids_empty(database_session: Session): """Test getting all editions from a user when there are none""" - user = models.User(name="test", email="test@email.com") + user = models.User(name="test") database_session.add(user) database_session.commit() @@ -72,7 +72,7 @@ def test_get_user_edition_ids_empty(database_session: Session): def test_get_user_edition_ids(database_session: Session): """Test getting all editions from a user when they aren't empty""" - user = models.User(name="test", email="test@email.com") + user = models.User(name="test") database_session.add(user) database_session.commit() @@ -81,7 +81,7 @@ def test_get_user_edition_ids(database_session: Session): assert len(editions) == 0 # Add user to a new edition - edition = models.Edition(year=2022) + edition = models.Edition(year=2022, name="ed2022") user.editions.append(edition) database_session.add(edition) database_session.add(user) From 6c592d10a5c2b342228551a200d03d7596647dab Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sat, 2 Apr 2022 15:38:45 +0200 Subject: [PATCH 184/536] Replace id's with names in backend --- backend/src/app/logic/users.py | 6 +++--- backend/src/app/schemas/login.py | 5 ++--- backend/src/database/crud/users.py | 7 +++---- backend/tests/test_database/test_crud/test_users.py | 13 ++++++------- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index aa9b1743c..b5ba5b772 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -25,9 +25,9 @@ def get_users_list(db: Session, admin: bool, edition_name: str | None) -> UsersL return UsersListResponse(users=users_orm) -def get_user_editions(user: User) -> list[int]: - """Get all id's of the editions this user is coach in""" - return users_crud.get_user_edition_ids(user) +def get_user_editions(user: User) -> list[str]: + """Get all names of the editions this user is coach in""" + return users_crud.get_user_edition_names(user) def edit_admin_status(db: Session, user_id: int, admin: AdminPatch): diff --git a/backend/src/app/schemas/login.py b/backend/src/app/schemas/login.py index 916daf416..02a6514ca 100644 --- a/backend/src/app/schemas/login.py +++ b/backend/src/app/schemas/login.py @@ -3,11 +3,10 @@ class UserData(CamelCaseModel): """User information that can be passed to frontend - Includes the id's of the editions a user is coach in - TODO replace with names once id-name change is merged + Includes the names of the editions a user is coach in """ admin: bool - editions: list[int] = [] + editions: list[str] = [] class Config: orm_mode = True diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 3ae256b62..04158c940 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -18,10 +18,9 @@ def get_all_users(db: Session) -> list[User]: return db.query(User).all() -def get_user_edition_ids(user: User) -> list[int]: - """Get all id's of the editions this user is coach in""" - # TODO replace with name - return list(map(lambda e: e.edition_id, user.editions)) +def get_user_edition_names(user: User) -> list[str]: + """Get all names of the editions this user is coach in""" + return list(map(lambda e: e.name, user.editions)) def get_users_from_edition(db: Session, edition_name: str) -> list[User]: diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index d302bb92b..5e06e6ed3 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -59,25 +59,25 @@ def test_get_all_admins(database_session: Session, data: dict[str, str]): assert data["user1"] == users[0].user_id -def test_get_user_edition_ids_empty(database_session: Session): +def test_get_user_edition_names_empty(database_session: Session): """Test getting all editions from a user when there are none""" user = models.User(name="test") database_session.add(user) database_session.commit() # No editions yet - editions = users_crud.get_user_edition_ids(user) + editions = users_crud.get_user_edition_names(user) assert len(editions) == 0 -def test_get_user_edition_ids(database_session: Session): +def test_get_user_edition_names(database_session: Session): """Test getting all editions from a user when they aren't empty""" user = models.User(name="test") database_session.add(user) database_session.commit() # No editions yet - editions = users_crud.get_user_edition_ids(user) + editions = users_crud.get_user_edition_names(user) assert len(editions) == 0 # Add user to a new edition @@ -88,8 +88,8 @@ def test_get_user_edition_ids(database_session: Session): database_session.commit() # No editions yet - editions = users_crud.get_user_edition_ids(user) - assert editions == [edition.edition_id] + editions = users_crud.get_user_edition_names(user) + assert editions == [edition.name] def test_get_all_users_from_edition(database_session: Session, data: dict[str, str]): @@ -177,7 +177,6 @@ def test_remove_coach(database_session: Session): def test_get_all_requests(database_session: Session): """Test get request for all userrequests""" - # Create user user1 = models.User(name="user1") user2 = models.User(name="user2") From 8e26bc62a871da2c2a80b8b6d61a4685341afbe8 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sat, 2 Apr 2022 16:48:59 +0200 Subject: [PATCH 185/536] Use edition names instead of id's in frontend --- frontend/src/Router.tsx | 5 ++-- .../components/PrivateRoute/PrivateRoute.tsx | 2 +- frontend/src/components/navbar/NavBar.tsx | 13 ++++----- frontend/src/data/interfaces/users.ts | 2 +- frontend/src/utils/api/auth.ts | 28 +++++++------------ .../src/views/RegisterPage/RegisterPage.tsx | 2 +- 6 files changed, 21 insertions(+), 31 deletions(-) diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 4ceb0df5a..828409fa2 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -17,13 +17,12 @@ import { NotFoundPage } from "./views/errors"; import ForbiddenPage from "./views/errors/ForbiddenPage"; export default function Router() { - const { token, setToken, isLoggedIn } = useAuth(); + const { isLoggedIn } = useAuth(); return ( - {/* TODO don't pass token & setToken but use useAuth */} - {isLoggedIn && } + {isLoggedIn && } {isLoggedIn === null ? ( // Busy verifying the access token diff --git a/frontend/src/components/PrivateRoute/PrivateRoute.tsx b/frontend/src/components/PrivateRoute/PrivateRoute.tsx index 4972a39ad..94a09f770 100644 --- a/frontend/src/components/PrivateRoute/PrivateRoute.tsx +++ b/frontend/src/components/PrivateRoute/PrivateRoute.tsx @@ -6,6 +6,6 @@ import { Navigate, Outlet } from "react-router-dom"; */ export default function PrivateRoute() { const { isLoggedIn } = useAuth(); - // TODO check edition once routes have been moved over + // TODO check edition existence & access once routes have been moved over return isLoggedIn ? : ; } diff --git a/frontend/src/components/navbar/NavBar.tsx b/frontend/src/components/navbar/NavBar.tsx index 033cad8f8..76a3e54cb 100644 --- a/frontend/src/components/navbar/NavBar.tsx +++ b/frontend/src/components/navbar/NavBar.tsx @@ -1,12 +1,11 @@ import React from "react"; -import { Nav, NavLink, Bars, NavMenu } from "./NavBarElements"; +import { Bars, Nav, NavLink, NavMenu } from "./NavBarElements"; import "./navbar.css"; +import { useAuth } from "../../contexts/auth-context"; -function NavBar({ token }: any, { setToken }: any) { - let hidden = "nav-hidden"; - if (token) { - hidden = "nav-links"; - } +function NavBar() { + const { token, setToken } = useAuth(); + const hidden = token ? "nav-links" : "nav-hidden"; return ( <> @@ -29,7 +28,7 @@ function NavBar({ token }: any, { setToken }: any) { { - setToken(""); + setToken(null); }} > Log out diff --git a/frontend/src/data/interfaces/users.ts b/frontend/src/data/interfaces/users.ts index 04114d5a2..38b485eac 100644 --- a/frontend/src/data/interfaces/users.ts +++ b/frontend/src/data/interfaces/users.ts @@ -2,5 +2,5 @@ export interface User { userId: number; name: string; admin: boolean; - editions: number[]; + editions: string[]; } diff --git a/frontend/src/utils/api/auth.ts b/frontend/src/utils/api/auth.ts index beea2e4b3..ef3bd0f49 100644 --- a/frontend/src/utils/api/auth.ts +++ b/frontend/src/utils/api/auth.ts @@ -10,24 +10,16 @@ export async function validateBearerToken(token: string | null): Promise can't validate anything if (token === null) return null; - // TODO uncomment once it works - // try { - // const response = await axiosInstance.get("/users/current"); - // return response.data as User; - // } catch (error) { - // if (axios.isAxiosError(error)) { - // return null; - // } else { - // throw error; - // } - // } - - return { - userId: 1, - name: "admin", - admin: false, - editions: [], - }; + try { + const response = await axiosInstance.get("/users/current"); + return response.data as User; + } catch (error) { + if (axios.isAxiosError(error)) { + return null; + } else { + throw error; + } + } } /** diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index 745d6d18e..050cc49b1 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -48,7 +48,7 @@ function RegisterPage() { } // TODO this has to change to get the edition the invite belongs to - const edition = "1"; + const edition = "ed2022"; try { const response = await register(edition, email, name, uuid, password); if (response) { From 61ffc9258fcf091deecfa558b721c7eefd890702 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sat, 2 Apr 2022 18:03:43 +0200 Subject: [PATCH 186/536] Manage admins --- frontend/package.json | 1 + frontend/src/App.tsx | 2 + frontend/src/utils/api/users/admins.ts | 44 ++++ frontend/src/utils/api/users/coaches.ts | 2 +- frontend/src/utils/api/users/users.ts | 61 ++++- .../src/views/AdminsPage/Admins/Admins.tsx | 240 ++++++++++++++++++ frontend/src/views/AdminsPage/Admins/index.ts | 1 + .../src/views/AdminsPage/Admins/styles.ts | 25 ++ frontend/src/views/AdminsPage/AdminsPage.tsx | 13 + .../src/views/UsersPage/Coaches/Coaches.tsx | 2 +- .../views/UsersPage/InviteUser/InviteUser.tsx | 6 +- .../src/views/UsersPage/InviteUser/styles.ts | 17 +- .../PendingRequests/PendingRequests.tsx | 2 +- frontend/src/views/UsersPage/UsersPage.tsx | 14 +- frontend/src/views/UsersPage/styles.ts | 18 ++ frontend/yarn.lock | 76 +++++- 16 files changed, 497 insertions(+), 27 deletions(-) create mode 100644 frontend/src/utils/api/users/admins.ts create mode 100644 frontend/src/views/AdminsPage/Admins/Admins.tsx create mode 100644 frontend/src/views/AdminsPage/Admins/index.ts create mode 100644 frontend/src/views/AdminsPage/Admins/styles.ts create mode 100644 frontend/src/views/AdminsPage/AdminsPage.tsx create mode 100644 frontend/src/views/UsersPage/styles.ts diff --git a/frontend/package.json b/frontend/package.json index 459e2ad6e..a032c5dec 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "bootstrap": "5.1.3", "react": "^17.0.2", "react-bootstrap": "^2.2.1", + "react-bootstrap-typeahead": "^6.0.0-alpha.11", "react-collapsible": "^2.8.4", "react-dom": "^17.0.2", "react-icons": "^4.3.1", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f588c5a38..23b162d57 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,7 @@ import NavBar from "./components/navbar"; import LoginPage from "./views/LoginPage/LoginPage"; import Students from "./views/Students"; import UsersPage from "./views/UsersPage/UsersPage"; +import AdminsPage from "./views/AdminsPage/AdminsPage"; import ProjectsPage from "./views/ProjectsPage"; import RegisterForm from "./views/RegisterForm/RegisterForm"; import ErrorPage from "./views/ErrorPage"; @@ -27,6 +28,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/utils/api/users/admins.ts b/frontend/src/utils/api/users/admins.ts new file mode 100644 index 000000000..437e1eb56 --- /dev/null +++ b/frontend/src/utils/api/users/admins.ts @@ -0,0 +1,44 @@ +import { User } from "./users"; + +export interface GetAdminsResponse { + admins: User[]; +} + +export async function getAdmins(): Promise { + const data = { + admins: [ + { + id: 5, + name: "Ward", + email: "ward@mail.be", + admin: true, + }, + { + id: 6, + name: "Francis", + email: "francis@mail.be", + admin: true, + }, + { + id: 7, + name: "Clement", + email: "clement@mail.be", + admin: true, + }, + ], + }; + + // eslint-disable-next-line promise/param-names + const delay = () => new Promise(res => setTimeout(res, 100)); + await delay(); + + return data; +} + +export async function addAdmin(userId: number) { + alert("add " + userId + " as admin"); +} + +export async function removeAdmin(userId: number) { + alert("remove " + userId + " as admin"); +} diff --git a/frontend/src/utils/api/users/coaches.ts b/frontend/src/utils/api/users/coaches.ts index 880e6d3e7..43fc40bda 100644 --- a/frontend/src/utils/api/users/coaches.ts +++ b/frontend/src/utils/api/users/coaches.ts @@ -33,6 +33,6 @@ export async function removeCoachFromEdition(userId: number, edition: string) { alert("remove " + userId + " from " + edition); } -export async function removeCoachFromAllEditions(userId: Number) { +export async function removeCoachFromAllEditions(userId: number) { alert("remove " + userId + " from all editions"); } diff --git a/frontend/src/utils/api/users/users.ts b/frontend/src/utils/api/users/users.ts index fdc5daa8c..5a3bdfb14 100644 --- a/frontend/src/utils/api/users/users.ts +++ b/frontend/src/utils/api/users/users.ts @@ -11,7 +11,7 @@ export interface User { /** * Get invite link for given email and edition */ -export async function getInviteLink(edition: string | undefined, email: string): Promise { +export async function getInviteLink(edition: string, email: string): Promise { try { await axiosInstance .post(`/editions/${edition}/invites/`, { email: email }) @@ -27,3 +27,62 @@ export async function getInviteLink(edition: string | undefined, email: string): } return ""; } + +export interface GetUsersResponse { + users: User[]; +} + +export async function getUsers(): Promise { + const data = { + users: [ + { + id: 1, + name: "Seppe", + email: "seppe@mail.be", + admin: false, + }, + { + id: 2, + name: "Stijn", + email: "stijn@mail.be", + admin: false, + }, + { + id: 3, + name: "Bert", + email: "bert@mail.be", + admin: false, + }, + { + id: 4, + name: "Tiebe", + email: "tiebe@mail.be", + admin: false, + }, + { + id: 5, + name: "Ward", + email: "ward@mail.be", + admin: true, + }, + { + id: 6, + name: "Francis", + email: "francis@mail.be", + admin: true, + }, + { + id: 7, + name: "Clement", + email: "clement@mail.be", + admin: true, + }, + ], + }; + + // eslint-disable-next-line promise/param-names + const delay = () => new Promise(res => setTimeout(res, 100)); + await delay(); + + return data; +} diff --git a/frontend/src/views/AdminsPage/Admins/Admins.tsx b/frontend/src/views/AdminsPage/Admins/Admins.tsx new file mode 100644 index 000000000..39df4af38 --- /dev/null +++ b/frontend/src/views/AdminsPage/Admins/Admins.tsx @@ -0,0 +1,240 @@ +import React, { useEffect, useState } from "react"; +import { AdminsContainer, AdminsTable, ModalContent, AddAdminButton, Warning } from "./styles"; +import { getUsers, User } from "../../../utils/api/users/users"; +import { Button, Modal, Spinner } from "react-bootstrap"; +import { addAdmin, getAdmins, removeAdmin } from "../../../utils/api/users/admins"; +import { SearchInput, SpinnerContainer } from "../../UsersPage/PendingRequests/styles"; +import { Typeahead } from "react-bootstrap-typeahead"; + +function AdminFilter(props: { + search: boolean; + searchTerm: string; + filter: (key: string) => void; +}) { + return props.filter(e.target.value)} />; +} + +function AddWarning(props: { name: string | undefined }) { + if (props.name !== undefined) { + return Warning: {props.name} will be able to edit/delete all data. ; + } + return null; +} + +function AddAdmin(props: { users: User[] }) { + const [show, setShow] = useState(false); + const [selected, setSelected] = useState(undefined); + + const handleClose = () => { + setSelected(undefined); + setShow(false); + }; + const handleShow = () => setShow(true); + + return ( + <> + + Add admin + + + + + + Add Admin + + + { + // @ts-ignore + setSelected(selected[0]); + }} + options={props.users} + labelKey={user => `${user.name} (${user.email})`} + filterBy={["email", "name"]} + /> + + + + + + + + + + ); +} + +function RemoveAdmin(props: { admin: User }) { + const [show, setShow] = useState(false); + + const handleClose = () => setShow(false); + const handleShow = () => setShow(true); + + return ( + <> + + + + + + Remove Admin + + +

{props.admin.name}

+

{props.admin.email}

+

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

+
+ + + + + +
+
+ + ); +} + +function AdminItem(props: { admin: User }) { + return ( + + {props.admin.name} + {props.admin.email} + + + + + ); +} + +function AdminsList(props: { admins: User[]; loading: boolean }) { + if (props.loading) { + return ( + + + + ); + } else if (props.admins.length === 0) { + return
No admins? #rip
; + } + + const body = ( + + {props.admins.map(admin => ( + + ))} + + ); + + return ( + + + + Name + Email + Remove + + + {body} + + ); +} + +export default function Admins() { + const [allAdmins, setAllAdmins] = useState([]); + const [admins, setAdmins] = useState([]); + const [users, setUsers] = useState([]); + const [gettingAdmins, setGettingAdmins] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [gotData, setGotData] = useState(false); + + useEffect(() => { + if (!gotData) { + getAdmins() + .then(response => { + setAdmins(response.admins); + setAllAdmins(response.admins); + setGettingAdmins(false); + setGotData(true); + }) + .catch(function (error: any) { + console.log(error); + setGettingAdmins(false); + }); + getUsers() + .then(response => { + const users = []; + for (const user of response.users) { + if (!allAdmins.some(e => e.id === user.id)) { + users.push(user); + } + } + setUsers(users); + }) + .catch(function (error: any) { + console.log(error); + }); + } + }); + + const filter = (word: string) => { + setSearchTerm(word); + const newCoaches: User[] = []; + for (const admin of allAdmins) { + if ( + admin.name.toUpperCase().includes(word.toUpperCase()) || + admin.email.toUpperCase().includes(word.toUpperCase()) + ) { + newCoaches.push(admin); + } + } + setAdmins(newCoaches); + }; + + return ( + + 0} + searchTerm={searchTerm} + filter={word => filter(word)} + /> + + + + ); +} diff --git a/frontend/src/views/AdminsPage/Admins/index.ts b/frontend/src/views/AdminsPage/Admins/index.ts new file mode 100644 index 000000000..549843890 --- /dev/null +++ b/frontend/src/views/AdminsPage/Admins/index.ts @@ -0,0 +1 @@ +export { default as Admins } from "./Admins"; diff --git a/frontend/src/views/AdminsPage/Admins/styles.ts b/frontend/src/views/AdminsPage/Admins/styles.ts new file mode 100644 index 000000000..75de16bb6 --- /dev/null +++ b/frontend/src/views/AdminsPage/Admins/styles.ts @@ -0,0 +1,25 @@ +import styled from "styled-components"; +import { Button, Table } from "react-bootstrap"; + +export const AdminsContainer = styled.div` + width: 50%; + min-width: 600px; + margin: 10px auto auto; +`; + +export const AdminsTable = styled(Table)``; + +export const ModalContent = styled.div` + border: 3px solid var(--osoc_green); + background-color: var(--osoc_blue); +`; + +export const AddAdminButton = styled(Button).attrs({ + size: "sm", +})` + float: right; +`; + +export const Warning = styled.div` + color: var(--osoc_red); +`; diff --git a/frontend/src/views/AdminsPage/AdminsPage.tsx b/frontend/src/views/AdminsPage/AdminsPage.tsx new file mode 100644 index 000000000..62ee215e0 --- /dev/null +++ b/frontend/src/views/AdminsPage/AdminsPage.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { Admins } from "./Admins"; + +function AdminsPage() { + return ( +
+

Manage admins

+ +
+ ); +} + +export default AdminsPage; diff --git a/frontend/src/views/UsersPage/Coaches/Coaches.tsx b/frontend/src/views/UsersPage/Coaches/Coaches.tsx index d5ec75481..38cc5f9fb 100644 --- a/frontend/src/views/UsersPage/Coaches/Coaches.tsx +++ b/frontend/src/views/UsersPage/Coaches/Coaches.tsx @@ -97,7 +97,7 @@ function CoachesList(props: { coaches: User[]; loading: boolean; edition: string const body = ( {props.coaches.map(coach => ( - + ))} ); diff --git a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx index 22b4339b5..ced2c13f9 100644 --- a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx +++ b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx @@ -8,11 +8,7 @@ function ButtonDiv(props: { loading: boolean; onClick: () => void }) { if (props.loading) { buttonDiv = ; } else { - buttonDiv = ( -
- Send invite -
- ); + buttonDiv = Send invite; } return buttonDiv; } diff --git a/frontend/src/views/UsersPage/InviteUser/styles.ts b/frontend/src/views/UsersPage/InviteUser/styles.ts index c7737b5ba..9d9e8a06c 100644 --- a/frontend/src/views/UsersPage/InviteUser/styles.ts +++ b/frontend/src/views/UsersPage/InviteUser/styles.ts @@ -1,16 +1,17 @@ import styled, { keyframes } from "styled-components"; +import { Button } from "react-bootstrap"; export const InviteContainer = styled.div` - overflow: hidden; + clear: both; `; export const InviteInput = styled.input.attrs({ name: "email", placeholder: "Invite user by email", })` - height: 35px; - width: 250px; - font-size: 15px; + height: 30px; + width: 200px; + font-size: 13px; margin-top: 10px; margin-left: 10px; text-align: center; @@ -19,14 +20,12 @@ export const InviteInput = styled.input.attrs({ float: left; `; -export const InviteButton = styled.button` - width: 90px; - height: 35px; +export const InviteButton = styled(Button).attrs({ + size: "sm", +})` cursor: pointer; background: var(--osoc_green); color: white; - border: none; - border-radius: 5px; margin-left: 7px; margin-top: 10px; `; diff --git a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx index 069713fbc..3b02b0766 100644 --- a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx +++ b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx @@ -60,7 +60,7 @@ function RequestsList(props: { requests: Request[]; loading: boolean }) { const body = ( {props.requests.map(request => ( - + ))} ); diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index 3740965ff..bd267f371 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -2,20 +2,28 @@ import React from "react"; import { InviteUser } from "./InviteUser"; import { PendingRequests } from "./PendingRequests"; import { Coaches } from "./Coaches"; -import { useParams } from "react-router-dom"; +import { useParams, useNavigate } from "react-router-dom"; +import { UsersPageDiv, AdminsButton, UsersHeader } from "./styles"; function UsersPage() { const params = useParams(); + const navigate = useNavigate(); if (params.edition === undefined) { return
Error
; } else { return ( -
+ +
+ +

Manage coaches from {params.edition}

+
+ navigate("/admins")}>Edit Admins +
-
+ ); } } diff --git a/frontend/src/views/UsersPage/styles.ts b/frontend/src/views/UsersPage/styles.ts new file mode 100644 index 000000000..d7adfe0ae --- /dev/null +++ b/frontend/src/views/UsersPage/styles.ts @@ -0,0 +1,18 @@ +import styled from "styled-components"; +import { Button } from "react-bootstrap"; + +export const UsersPageDiv = styled.div``; + +export const AdminsButton = styled(Button)` + background-color: var(--osoc_green); + margin-right: 10px; + margin-top: 10px; + float: right; +`; + +export const UsersHeader = styled.div` + padding-left: 10px; + margin-top: 10px; + float: left; + display: inline-block; +`; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 5232bfe5c..e76e6ed2d 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1024,7 +1024,7 @@ 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.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": +"@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== @@ -1454,7 +1454,7 @@ schema-utils "^3.0.0" source-map "^0.7.3" -"@popperjs/core@^2.10.1": +"@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" integrity sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg== @@ -1466,6 +1466,13 @@ 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" + 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" @@ -3057,7 +3064,7 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== -classnames@^2.3.1: +classnames@^2.2.0, classnames@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== @@ -3193,6 +3200,11 @@ compression@^1.7.4: safe-buffer "5.1.2" vary "~1.1.2" +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" + 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" @@ -5038,7 +5050,7 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" -invariant@^2.2.4: +invariant@^2.2.1, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -7236,7 +7248,7 @@ prop-types-extra@^1.1.0: react-is "^16.3.2" warning "^4.0.0" -prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.5.8, 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" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -7324,6 +7336,24 @@ react-app-polyfill@^3.0.0: regenerator-runtime "^0.13.9" whatwg-fetch "^3.6.2" +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" + integrity sha512-yHBPsdkAdvvLpkq6wWei55qt4REdbRnC+1wVxkBSBeTG4Z6lkKKSGx6w4kY9YmkyyVcbmmfwUSXhCWY/M+TzCg== + dependencies: + "@babel/runtime" "^7.14.6" + "@popperjs/core" "^2.10.2" + "@restart/hooks" "^0.4.0" + classnames "^2.2.0" + fast-deep-equal "^3.1.1" + invariant "^2.2.1" + lodash.debounce "^4.0.8" + prop-types "^15.5.8" + react-overlays "^5.1.0" + react-popper "^2.2.5" + 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" @@ -7395,6 +7425,11 @@ react-error-overlay@^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-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" + 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" @@ -7415,6 +7450,28 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" 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" + integrity sha512-eCN2s2/+GVZzpnId4XVWtvDPYYBD2EtOGP74hE+8yDskPzFy9+pV1H3ZZihxuRdEbQzzacySaaDkR7xE0ydl4Q== + dependencies: + "@babel/runtime" "^7.13.8" + "@popperjs/core" "^2.8.6" + "@restart/hooks" "^0.3.26" + "@types/warning" "^3.0.0" + dom-helpers "^5.2.0" + prop-types "^15.7.2" + uncontrollable "^7.2.1" + warning "^4.0.3" + +react-popper@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.5.tgz#1214ef3cec86330a171671a4fbcbeeb65ee58e96" + integrity sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw== + dependencies: + react-fast-compare "^3.0.1" + warning "^4.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" @@ -7842,6 +7899,13 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.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" + 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" @@ -8736,7 +8800,7 @@ walker@^1.0.7: dependencies: makeerror "1.0.12" -warning@^4.0.0, warning@^4.0.3: +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" integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== From ce1d3bf4f0dbf273d2f901df285642828c339f5e Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sat, 2 Apr 2022 20:26:13 +0200 Subject: [PATCH 187/536] Small fixes --- frontend/src/utils/api/users/admins.ts | 4 ++ .../src/views/AdminsPage/Admins/Admins.tsx | 39 +++++++++++++------ .../src/views/AdminsPage/Admins/styles.ts | 7 +++- .../PendingRequests/PendingRequests.tsx | 25 ++++++++++-- .../views/UsersPage/PendingRequests/styles.ts | 26 ++++++++++++- 5 files changed, 84 insertions(+), 17 deletions(-) diff --git a/frontend/src/utils/api/users/admins.ts b/frontend/src/utils/api/users/admins.ts index 437e1eb56..c22282509 100644 --- a/frontend/src/utils/api/users/admins.ts +++ b/frontend/src/utils/api/users/admins.ts @@ -42,3 +42,7 @@ export async function addAdmin(userId: number) { export async function removeAdmin(userId: number) { alert("remove " + userId + " as admin"); } + +export async function removeAdminAndCoach(userId: number) { + alert("remove " + userId + " as admin & coach"); +} diff --git a/frontend/src/views/AdminsPage/Admins/Admins.tsx b/frontend/src/views/AdminsPage/Admins/Admins.tsx index 39df4af38..97911dd1d 100644 --- a/frontend/src/views/AdminsPage/Admins/Admins.tsx +++ b/frontend/src/views/AdminsPage/Admins/Admins.tsx @@ -1,8 +1,20 @@ import React, { useEffect, useState } from "react"; -import { AdminsContainer, AdminsTable, ModalContent, AddAdminButton, Warning } from "./styles"; +import { + AdminsContainer, + AdminsTable, + ModalContentGreen, + ModalContentRed, + AddAdminButton, + Warning, +} from "./styles"; import { getUsers, User } from "../../../utils/api/users/users"; import { Button, Modal, Spinner } from "react-bootstrap"; -import { addAdmin, getAdmins, removeAdmin } from "../../../utils/api/users/admins"; +import { + addAdmin, + getAdmins, + removeAdmin, + removeAdminAndCoach, +} from "../../../utils/api/users/admins"; import { SearchInput, SpinnerContainer } from "../../UsersPage/PendingRequests/styles"; import { Typeahead } from "react-bootstrap-typeahead"; @@ -16,7 +28,11 @@ function AdminFilter(props: { function AddWarning(props: { name: string | undefined }) { if (props.name !== undefined) { - return Warning: {props.name} will be able to edit/delete all data. ; + return ( + + Warning: {props.name} will be able to edit/delete all data and manage admin roles. + + ); } return null; } @@ -38,19 +54,20 @@ function AddAdmin(props: { users: User[] }) { - + Add Admin { - // @ts-ignore - setSelected(selected[0]); + setSelected(selected[0] as User); }} options={props.users} - labelKey={user => `${user.name} (${user.email})`} + labelKey="email" filterBy={["email", "name"]} + emptyLabel="No users found." + placeholder={"email"} /> @@ -71,7 +88,7 @@ function AddAdmin(props: { users: User[] }) { Cancel - + ); @@ -90,7 +107,7 @@ function RemoveAdmin(props: { admin: User }) { - + Remove Admin @@ -114,7 +131,7 @@ function RemoveAdmin(props: { admin: User }) { - + ); diff --git a/frontend/src/views/AdminsPage/Admins/styles.ts b/frontend/src/views/AdminsPage/Admins/styles.ts index 75de16bb6..3f0534424 100644 --- a/frontend/src/views/AdminsPage/Admins/styles.ts +++ b/frontend/src/views/AdminsPage/Admins/styles.ts @@ -9,11 +9,16 @@ export const AdminsContainer = styled.div` export const AdminsTable = styled(Table)``; -export const ModalContent = styled.div` +export const ModalContentGreen = styled.div` border: 3px solid var(--osoc_green); background-color: var(--osoc_blue); `; +export const ModalContentRed = styled.div` + border: 3px solid var(--osoc_red); + background-color: var(--osoc_blue); +`; + export const AddAdminButton = styled(Button).attrs({ size: "sm", })` diff --git a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx index 3b02b0766..44eb40d64 100644 --- a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx +++ b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx @@ -2,12 +2,16 @@ import React, { useEffect, useState } from "react"; import Collapsible from "react-collapsible"; import { RequestHeaderTitle, + RequestHeaderDiv, + OpenArrow, + ClosedArrow, RequestsTable, PendingRequestsContainer, AcceptButton, RejectButton, SpinnerContainer, SearchInput, + AcceptRejectTh, } from "./styles"; import { acceptRequest, @@ -17,8 +21,21 @@ import { } from "../../../utils/api/users/requests"; import { Spinner } from "react-bootstrap"; +function Arrow(props: { open: boolean }) { + if (props.open) { + return ; + } else { + return ; + } +} + function RequestHeader(props: { open: boolean }) { - return Requests; + return ( + + Requests + + + ); } function RequestFilter(props: { searchTerm: string; filter: (key: string) => void }) { @@ -71,7 +88,7 @@ function RequestsList(props: { requests: Request[]; loading: boolean }) { Name Email - Accept/Reject + Accept/Reject {body} @@ -121,8 +138,8 @@ export default function PendingRequests(props: { edition: string }) { } - onOpen={() => setOpen(true)} - onClose={() => setOpen(false)} + onOpening={() => setOpen(true)} + onClosing={() => setOpen(false)} > filter(word)} /> diff --git a/frontend/src/views/UsersPage/PendingRequests/styles.ts b/frontend/src/views/UsersPage/PendingRequests/styles.ts index 6c004c5ff..5cf5e95b0 100644 --- a/frontend/src/views/UsersPage/PendingRequests/styles.ts +++ b/frontend/src/views/UsersPage/PendingRequests/styles.ts @@ -1,18 +1,36 @@ import styled from "styled-components"; import { Table, Button } from "react-bootstrap"; +import { BiDownArrow } from "react-icons/bi"; + +export const RequestHeaderDiv = styled.div` + display: inline-block; +`; export const RequestHeaderTitle = styled.div` padding-bottom: 3px; padding-left: 3px; width: 100px; font-size: 25px; + float: left; +`; + +export const OpenArrow = styled(BiDownArrow)` + margin-top: 13px; + margin-left: 10px; + offset-position: 0px 30px; +`; + +export const ClosedArrow = styled(BiDownArrow)` + margin-top: 13px; + margin-left: 10px; + transform: rotate(-90deg); + offset: 0px 30px; `; export const SearchInput = styled.input.attrs({ placeholder: "Search", })` margin: 3px; - height: 20px; width: 150px; font-size: 15px; border-radius: 5px; @@ -27,12 +45,17 @@ export const PendingRequestsContainer = styled.div` margin: 10px auto auto; `; +export const AcceptRejectTh = styled.th` + width: 150px; +`; + 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)` @@ -42,6 +65,7 @@ export const RejectButton = styled(Button)` padding-bottom: 3px; padding-left: 3px; padding-right: 3px; + width: 65px; `; export const SpinnerContainer = styled.div` From 110e86a0f9e68c71e3de4cb5470e8297a7f959c0 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sat, 2 Apr 2022 20:42:07 +0200 Subject: [PATCH 188/536] Add coach to edition --- frontend/src/utils/api/users/coaches.ts | 4 + .../src/views/AdminsPage/Admins/Admins.tsx | 2 +- .../src/views/UsersPage/Coaches/Coaches.tsx | 78 ++++++++++++++++++- .../src/views/UsersPage/Coaches/styles.ts | 17 ---- 4 files changed, 81 insertions(+), 20 deletions(-) diff --git a/frontend/src/utils/api/users/coaches.ts b/frontend/src/utils/api/users/coaches.ts index 43fc40bda..b55116cac 100644 --- a/frontend/src/utils/api/users/coaches.ts +++ b/frontend/src/utils/api/users/coaches.ts @@ -36,3 +36,7 @@ export async function removeCoachFromEdition(userId: number, edition: string) { export async function removeCoachFromAllEditions(userId: number) { alert("remove " + userId + " from all editions"); } + +export async function addCoachToEdition(userId: number, edition: string) { + alert("add " + userId + " to " + edition); +} diff --git a/frontend/src/views/AdminsPage/Admins/Admins.tsx b/frontend/src/views/AdminsPage/Admins/Admins.tsx index 97911dd1d..e0651636f 100644 --- a/frontend/src/views/AdminsPage/Admins/Admins.tsx +++ b/frontend/src/views/AdminsPage/Admins/Admins.tsx @@ -102,7 +102,7 @@ function RemoveAdmin(props: { admin: User }) { return ( <> - diff --git a/frontend/src/views/UsersPage/Coaches/Coaches.tsx b/frontend/src/views/UsersPage/Coaches/Coaches.tsx index 38cc5f9fb..4c6c72ea5 100644 --- a/frontend/src/views/UsersPage/Coaches/Coaches.tsx +++ b/frontend/src/views/UsersPage/Coaches/Coaches.tsx @@ -1,13 +1,16 @@ import React, { useEffect, useState } from "react"; import { CoachesTitle, CoachesContainer, CoachesTable, ModalContent } from "./styles"; -import { User } from "../../../utils/api/users/users"; +import { getUsers, User } from "../../../utils/api/users/users"; import { SearchInput, SpinnerContainer } from "../PendingRequests/styles"; import { Button, Modal, Spinner } from "react-bootstrap"; import { getCoaches, removeCoachFromAllEditions, removeCoachFromEdition, + addCoachToEdition, } from "../../../utils/api/users/coaches"; +import { AddAdminButton, ModalContentGreen } from "../../AdminsPage/Admins/styles"; +import { Typeahead } from "react-bootstrap-typeahead"; function CoachesHeader() { return Coaches; @@ -21,6 +24,62 @@ function CoachFilter(props: { return props.filter(e.target.value)} />; } +function AddCoach(props: { users: User[]; edition: string }) { + const [show, setShow] = useState(false); + const [selected, setSelected] = useState(undefined); + + const handleClose = () => { + setSelected(undefined); + setShow(false); + }; + const handleShow = () => setShow(true); + + return ( + <> + + Add coach + + + + + + Add Coach + + + { + setSelected(selected[0] as User); + }} + options={props.users} + labelKey="email" + filterBy={["email", "name"]} + emptyLabel="No users found." + placeholder={"user's email address"} + /> + + + + + + + + + ); +} + function RemoveCoach(props: { coach: User; edition: string }) { const [show, setShow] = useState(false); @@ -29,7 +88,7 @@ function RemoveCoach(props: { coach: User; edition: string }) { return ( <> - @@ -119,6 +178,7 @@ function CoachesList(props: { coaches: User[]; loading: boolean; edition: string export default function Coaches(props: { edition: string }) { const [allCoaches, setAllCoaches] = useState([]); const [coaches, setCoaches] = useState([]); + const [users, setUsers] = useState([]); const [gettingCoaches, setGettingCoaches] = useState(true); const [searchTerm, setSearchTerm] = useState(""); const [gotData, setGotData] = useState(false); @@ -136,6 +196,19 @@ export default function Coaches(props: { edition: string }) { console.log(error); setGettingCoaches(false); }); + getUsers() + .then(response => { + const users = []; + for (const user of response.users) { + if (!allCoaches.some(e => e.id === user.id)) { + users.push(user); + } + } + setUsers(users); + }) + .catch(function (error: any) { + console.log(error); + }); } }); @@ -161,6 +234,7 @@ export default function Coaches(props: { edition: string }) { searchTerm={searchTerm} filter={word => filter(word)} /> + ); diff --git a/frontend/src/views/UsersPage/Coaches/styles.ts b/frontend/src/views/UsersPage/Coaches/styles.ts index 1f8970bd5..1d388b8f1 100644 --- a/frontend/src/views/UsersPage/Coaches/styles.ts +++ b/frontend/src/views/UsersPage/Coaches/styles.ts @@ -14,25 +14,8 @@ export const CoachesTitle = styled.div` font-size: 25px; `; -export const RemoveFromEditionButton = styled.button` - background-color: var(--osoc_red); - margin-left: 3px; - padding-bottom: 3px; - padding-left: 3px; - padding-right: 3px; -`; - export const CoachesTable = styled(Table)``; -export const PopupDiv = styled.div` - background-color: var(--osoc_red); - width: 200px; - height: 100px; - position: absolute; - right: 0; - top: 0; -`; - export const ModalContent = styled.div` border: 3px solid var(--osoc_red); background-color: var(--osoc_blue); From 1261724d3a199cf1e04d38ee4385d2a0dc686c08 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 3 Apr 2022 12:10:58 +0200 Subject: [PATCH 189/536] Get requests api --- frontend/src/utils/api/users/requests.ts | 42 +------------ .../PendingRequests/PendingRequests.tsx | 63 +++++++++++++------ .../views/UsersPage/PendingRequests/styles.ts | 4 ++ 3 files changed, 51 insertions(+), 58 deletions(-) diff --git a/frontend/src/utils/api/users/requests.ts b/frontend/src/utils/api/users/requests.ts index a1716e187..ad2282cc8 100644 --- a/frontend/src/utils/api/users/requests.ts +++ b/frontend/src/utils/api/users/requests.ts @@ -1,4 +1,5 @@ import { User } from "./users"; +import { axiosInstance } from "../api"; export interface Request { id: number; @@ -10,45 +11,8 @@ export interface GetRequestsResponse { } export async function getRequests(edition: string): Promise { - const data = { - requests: [ - { - id: 1, - user: { - id: 1, - name: "Seppe", - email: "seppe@mail.be", - admin: false, - }, - }, - { - id: 2, - user: { - id: 2, - name: "Stijn", - email: "stijn@mail.be", - admin: false, - }, - }, - ], - }; - - // eslint-disable-next-line promise/param-names - const delay = () => new Promise(res => setTimeout(res, 1000)); - await delay(); - - return data; - - // try { - // await axiosInstance - // .get(`/users/requests/?edition=${edition}`) - // .then(response => { - // return response.data; - // } - // ) - // } catch (error) { - // - // } + const response = await axiosInstance.get(`/users/requests/?edition=${edition}`); + return response.data as GetRequestsResponse; } export async function acceptRequest(requestId: number) { diff --git a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx index 44eb40d64..dd887749f 100644 --- a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx +++ b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx @@ -12,6 +12,7 @@ import { SpinnerContainer, SearchInput, AcceptRejectTh, + Error, } from "./styles"; import { acceptRequest, @@ -38,8 +39,18 @@ function RequestHeader(props: { open: boolean }) { ); } -function RequestFilter(props: { searchTerm: string; filter: (key: string) => void }) { - return props.filter(e.target.value)} />; +function RequestFilter(props: { + searchTerm: string; + filter: (key: string) => void; + show: boolean; +}) { + if (props.show) { + return ( + props.filter(e.target.value)} /> + ); + } else { + return null; + } } function AcceptReject(props: { requestId: number }) { @@ -63,7 +74,7 @@ function RequestItem(props: { request: Request }) { ); } -function RequestsList(props: { requests: Request[]; loading: boolean }) { +function RequestsList(props: { requests: Request[]; loading: boolean; gotData: boolean }) { if (props.loading) { return ( @@ -71,7 +82,11 @@ function RequestsList(props: { requests: Request[]; loading: boolean }) { ); } else if (props.requests.length === 0) { - return
No requests
; + if (props.gotData) { + return
No requests
; + } else { + return null; + } } const body = ( @@ -99,24 +114,29 @@ function RequestsList(props: { requests: Request[]; loading: boolean }) { export default function PendingRequests(props: { edition: string }) { const [allRequests, setAllRequests] = useState([]); const [requests, setRequests] = useState([]); - const [gettingRequests, setGettingRequests] = useState(true); + const [gettingRequests, setGettingRequests] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [gotData, setGotData] = useState(false); const [open, setOpen] = useState(false); + const [error, setError] = useState(""); + + async function getData() { + try { + const response = await getRequests(props.edition); + setAllRequests(response.requests); + setRequests(response.requests); + setGotData(true); + setGettingRequests(false); + } catch (exception) { + setError("Oops, something went wrong..."); + setGettingRequests(false); + } + } useEffect(() => { - if (!gotData) { - getRequests(props.edition) - .then(response => { - setRequests(response.requests); - setAllRequests(response.requests); - setGettingRequests(false); - setGotData(true); - }) - .catch(function (error: any) { - console.log(error); - setGettingRequests(false); - }); + if (!gotData && !gettingRequests && !error) { + setGettingRequests(true); + getData(); } }); @@ -141,8 +161,13 @@ export default function PendingRequests(props: { edition: string }) { onOpening={() => setOpen(true)} onClosing={() => setOpen(false)} > - filter(word)} /> - + filter(word)} + show={requests.length > 0} + /> + + {error}
); diff --git a/frontend/src/views/UsersPage/PendingRequests/styles.ts b/frontend/src/views/UsersPage/PendingRequests/styles.ts index 5cf5e95b0..03be79ef6 100644 --- a/frontend/src/views/UsersPage/PendingRequests/styles.ts +++ b/frontend/src/views/UsersPage/PendingRequests/styles.ts @@ -74,3 +74,7 @@ export const SpinnerContainer = styled.div` align-items: center; margin: 20px; `; + +export const Error = styled.div` + color: var(--osoc_red); +`; From 4abde31232d6117f009399cce2d94aa839b114a1 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 3 Apr 2022 12:11:32 +0200 Subject: [PATCH 190/536] show projectscard returned from api call --- .../ProjectCard/ProjectCard.tsx | 2 +- frontend/src/utils/api/projects.ts | 4 +-- .../src/views/ProjectsPage/ProjectsPage.tsx | 25 +++++++++++++++---- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 9e5fae533..826ad7ef2 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -21,7 +21,7 @@ export default function ProjectCard({

{client}

- {coaches.map((element, index) => ( + {coaches.map((element, _index) => ( {element} ))} diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index bf9ce358a..5c4c6982b 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -3,9 +3,7 @@ import { axiosInstance } from "./api"; export async function getProjects(edition: string) { try { - const response = await axiosInstance.get("/editions/" + edition + "/projects"); - console.log(response); - + const response = await axiosInstance.get("/editions/" + edition + "/projects"); const projects = response.data; return projects; } catch (error) { diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index 64287da75..0cdd6ae91 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -6,25 +6,40 @@ import { ProjectCard } from "../../components/ProjectsComponents"; import { CardsGrid } from "./styles"; +interface Project { + name: string; + partners: any[]; +} + function ProjectPage() { - const [projects, setProjects] = useState(); + const [projects, setProjects] = useState>([]); + const [gotProjects, setGotProjects] = useState(false); useEffect(() => { async function callProjects() { const response = await getProjects("1"); if (response) { - console.log(response); - setProjects(response); + setGotProjects(true); + setProjects(response.projects); } } - if (!projects) { + if (!gotProjects) { callProjects(); - } else console.log("hello"); + } }); return (
+ {projects.map((project, _index) => ( + + ))} + Date: Sun, 3 Apr 2022 12:55:59 +0200 Subject: [PATCH 191/536] Add header when verifying token --- frontend/src/utils/api/auth.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/api/auth.ts b/frontend/src/utils/api/auth.ts index ef3bd0f49..4a6638621 100644 --- a/frontend/src/utils/api/auth.ts +++ b/frontend/src/utils/api/auth.ts @@ -11,7 +11,14 @@ export async function validateBearerToken(token: string | null): Promise Date: Sun, 3 Apr 2022 12:59:14 +0200 Subject: [PATCH 192/536] fixed key property --- .../ProjectsComponents/ProjectCard/ProjectCard.tsx | 2 +- frontend/src/views/ProjectsPage/ProjectsPage.tsx | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 826ad7ef2..4772f57dc 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -22,7 +22,7 @@ export default function ProjectCard({

{client}

{coaches.map((element, _index) => ( - {element} + {element} ))} diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index 0cdd6ae91..e084fa9d5 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -33,10 +33,10 @@ function ProjectPage() { {projects.map((project, _index) => ( ))} @@ -50,17 +50,11 @@ function ProjectPage() { client="client 2" coaches={["Miet", "Bart", "Dirk de lange", "Jef de korte"]} /> - - - - - -
); From 374f81d75569a334bc4529aeb0e1028bcd6f1203 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 3 Apr 2022 13:12:04 +0200 Subject: [PATCH 193/536] Logout --- frontend/src/contexts/auth-context.tsx | 8 +++++--- frontend/src/utils/local-storage/auth.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx index 521a90b60..cb374ae27 100644 --- a/frontend/src/contexts/auth-context.tsx +++ b/frontend/src/contexts/auth-context.tsx @@ -61,11 +61,13 @@ export function AuthProvider({ children }: { children: ReactNode }) { setRole: setRole, token: token, setToken: (value: string | null) => { - // Set the token in LocalStorage - if (value) { - setTokenInStorage(value); + // Log the user out if token is null + if (value === null) { + setIsLoggedIn(false); } + // Set the token in LocalStorage + setTokenInStorage(value); setToken(value); }, editions: editions, diff --git a/frontend/src/utils/local-storage/auth.ts b/frontend/src/utils/local-storage/auth.ts index 908c0ff3a..6cb70be2f 100644 --- a/frontend/src/utils/local-storage/auth.ts +++ b/frontend/src/utils/local-storage/auth.ts @@ -3,8 +3,12 @@ import { StorageKey } from "../../data/enums"; /** * Write the new value of a token into LocalStorage */ -export function setToken(value: string) { - localStorage.setItem(StorageKey.BEARER_TOKEN, value); +export function setToken(value: string | null) { + if (value === null) { + localStorage.removeItem(StorageKey.BEARER_TOKEN); + } else { + localStorage.setItem(StorageKey.BEARER_TOKEN, value); + } } /** From 26372abd97507fb3b648cd5b1b5c68e2b07d40ea Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sat, 26 Mar 2022 18:09:17 +0100 Subject: [PATCH 194/536] Begin UsersPage --- frontend/src/utils/api/users.ts | 22 +++++++++ frontend/src/views/UsersPage/UsersPage.css | 57 ++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 frontend/src/utils/api/users.ts diff --git a/frontend/src/utils/api/users.ts b/frontend/src/utils/api/users.ts new file mode 100644 index 000000000..c8e53048a --- /dev/null +++ b/frontend/src/utils/api/users.ts @@ -0,0 +1,22 @@ +import axios from "axios"; +import { axiosInstance } from "./api"; + +/** + * Get invite link for given email and edition + */ +export async function getInviteLink(edition: string, email: string): Promise { + try { + await axiosInstance + .post(`/editions/${edition}/invites/`, { email: email }) + .then(response => { + return response.data.mailTo; + }); + } catch (error) { + if (axios.isAxiosError(error)) { + return error.message; + } else { + throw error; + } + } + return ""; +} diff --git a/frontend/src/views/UsersPage/UsersPage.css b/frontend/src/views/UsersPage/UsersPage.css index e69de29bb..9634fd692 100644 --- a/frontend/src/views/UsersPage/UsersPage.css +++ b/frontend/src/views/UsersPage/UsersPage.css @@ -0,0 +1,57 @@ +.invite-user-container { + overflow: hidden; +} + +.invite-user-container input[type="email"] { + height: 30px; + width: 200px; + margin-top: 10px; + margin-left: 10px; + text-align: center; + font-size: 15px; + border-radius: 5px; + border-width: 0; + float: left +} + +.invite-user-container button { + width: 90px; + height: 30px; + cursor: pointer; + background: var(--osoc_green); + color: white; + border: none; + border-radius: 5px; + margin-left: 7px; + margin-top: 10px; +} + +.email-field-error { + border: 2px solid red !important; +} + +.loader { + border: 8px solid var(--osoc_blue); + border-top: 8px solid var(--osoc_green); + border-radius: 50%; + width: 30px; + height: 30px; + animation: spin 2s linear infinite; + margin-left: 37px; + margin-top: 10px; + float: left; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.link { + margin-left: 10px; +} + +.error { + margin-left: 10px; + color: var(--osoc_red); +} From e39f0a736945b3515b3ca61be8b3fc9923e87d8b Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 27 Mar 2022 14:04:22 +0200 Subject: [PATCH 195/536] adapt to style guide --- .../views/UsersPage/InviteUser/InviteUser.tsx | 74 +++++++++++++++++++ .../UsersPage/InviteUser/InviteUsers.css | 4 + .../src/views/UsersPage/InviteUser/index.ts | 1 + .../src/views/UsersPage/InviteUser/styles.ts | 60 +++++++++++++++ frontend/src/views/UsersPage/UsersPage.css | 57 -------------- frontend/src/views/UsersPage/UsersPage.tsx | 3 +- 6 files changed, 141 insertions(+), 58 deletions(-) create mode 100644 frontend/src/views/UsersPage/InviteUser/InviteUser.tsx create mode 100644 frontend/src/views/UsersPage/InviteUser/InviteUsers.css create mode 100644 frontend/src/views/UsersPage/InviteUser/index.ts create mode 100644 frontend/src/views/UsersPage/InviteUser/styles.ts delete mode 100644 frontend/src/views/UsersPage/UsersPage.css diff --git a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx new file mode 100644 index 000000000..66d3add2b --- /dev/null +++ b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { getInviteLink } from "../../../utils/api/users"; +import "./InviteUsers.css"; +import { InviteInput, InviteButton, Loader, InviteContainer, Link, Error } from "./styles"; + +export default class InviteUser extends React.Component< + {}, + { + email: string; + valid: boolean; + errorMessage: string | null; + loading: boolean; + link: string | null; + } +> { + constructor(props = {}) { + super(props); + this.state = { email: "", valid: true, errorMessage: null, loading: false, link: null }; + } + + setEmail(email: string) { + this.setState({ email: email, valid: true, link: null, errorMessage: null }); + } + + async sendInvite() { + if (/[^@\s]+@[^@\s]+\.[^@\s]+/.test(this.state.email)) { + this.setState({ loading: true }); + getInviteLink("edition", this.state.email).then(ding => { + this.setState({ link: ding, loading: false }); // TODO: fix email stuff + }); + } else { + this.setState({ valid: false, errorMessage: "Invalid email" }); + } + } + + render() { + let button; + if (this.state.loading) { + button = ; + } else { + button = ( +
+ this.sendInvite()}>Send invite +
+ ); + } + + let error = null; + if (this.state.errorMessage) { + error = {this.state.errorMessage}; + } + + let link = null; + if (this.state.link) { + link = {this.state.link}; + } + + return ( +
+ + this.setEmail(e.target.value)} + /> + {button} + + {error} + {link} +
+ ); + } +} diff --git a/frontend/src/views/UsersPage/InviteUser/InviteUsers.css b/frontend/src/views/UsersPage/InviteUser/InviteUsers.css new file mode 100644 index 000000000..bcbfb0d0f --- /dev/null +++ b/frontend/src/views/UsersPage/InviteUser/InviteUsers.css @@ -0,0 +1,4 @@ + +.email-field-error { + border: 2px solid red !important; +} diff --git a/frontend/src/views/UsersPage/InviteUser/index.ts b/frontend/src/views/UsersPage/InviteUser/index.ts new file mode 100644 index 000000000..f268c1378 --- /dev/null +++ b/frontend/src/views/UsersPage/InviteUser/index.ts @@ -0,0 +1 @@ +export { default as InviteUser } from "./InviteUser"; diff --git a/frontend/src/views/UsersPage/InviteUser/styles.ts b/frontend/src/views/UsersPage/InviteUser/styles.ts new file mode 100644 index 000000000..56d0bb6b3 --- /dev/null +++ b/frontend/src/views/UsersPage/InviteUser/styles.ts @@ -0,0 +1,60 @@ +import styled, { keyframes } from "styled-components"; + +export const InviteContainer = styled.div` + overflow: hidden; +`; + +export const InviteInput = styled.input` + height: 35px; + width: 250px; + font-size: 15px; + margin-top: 10px; + margin-left: 10px; + text-align: center; + border-radius: 5px; + border-width: 0; + float: left; +`; + +export const InviteButton = styled.button` + width: 90px; + height: 35px; + cursor: pointer; + background: var(--osoc_green); + color: white; + border: none; + border-radius: 5px; + margin-left: 7px; + margin-top: 10px; +`; + +const rotate = keyframes` + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +`; + +export const Loader = styled.div` + border: 8px solid var(--osoc_green); + border-top: 8px solid var(--osoc_blue); + border-radius: 50%; + width: 35px; + height: 35px; + animation: ${rotate} 2s linear infinite; + margin-left: 37px; + margin-top: 10px; + float: left; +`; + +export const Link = styled.div` + margin-left: 10px; +`; + +export const Error = styled.div` + margin-left: 10px; + color: var(--osoc_red); +`; diff --git a/frontend/src/views/UsersPage/UsersPage.css b/frontend/src/views/UsersPage/UsersPage.css deleted file mode 100644 index 9634fd692..000000000 --- a/frontend/src/views/UsersPage/UsersPage.css +++ /dev/null @@ -1,57 +0,0 @@ -.invite-user-container { - overflow: hidden; -} - -.invite-user-container input[type="email"] { - height: 30px; - width: 200px; - margin-top: 10px; - margin-left: 10px; - text-align: center; - font-size: 15px; - border-radius: 5px; - border-width: 0; - float: left -} - -.invite-user-container button { - width: 90px; - height: 30px; - cursor: pointer; - background: var(--osoc_green); - color: white; - border: none; - border-radius: 5px; - margin-left: 7px; - margin-top: 10px; -} - -.email-field-error { - border: 2px solid red !important; -} - -.loader { - border: 8px solid var(--osoc_blue); - border-top: 8px solid var(--osoc_green); - border-radius: 50%; - width: 30px; - height: 30px; - animation: spin 2s linear infinite; - margin-left: 37px; - margin-top: 10px; - float: left; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -.link { - margin-left: 10px; -} - -.error { - margin-left: 10px; - color: var(--osoc_red); -} diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index b96c7fc57..ed9a12a2b 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -1,7 +1,8 @@ import React from "react"; +import { InviteUser } from "./InviteUser"; function UsersPage() { - return
This is the users page
; + return ; } export default UsersPage; From 26e202005e46124f0a8b84e300f1df605e1485d2 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 27 Mar 2022 14:39:38 +0200 Subject: [PATCH 196/536] convert class component to functional component --- .../views/UsersPage/InviteUser/InviteUser.tsx | 105 +++++++++--------- 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx index 66d3add2b..74dcf122e 100644 --- a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx +++ b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx @@ -1,74 +1,79 @@ -import React from "react"; +import React, { useState } from "react"; import { getInviteLink } from "../../../utils/api/users"; import "./InviteUsers.css"; import { InviteInput, InviteButton, Loader, InviteContainer, Link, Error } from "./styles"; -export default class InviteUser extends React.Component< - {}, - { - email: string; - valid: boolean; - errorMessage: string | null; - loading: boolean; - link: string | null; - } -> { - constructor(props = {}) { - super(props); - this.state = { email: "", valid: true, errorMessage: null, loading: false, link: null }; - } +export default function InviteUser() { + const [email, setEmail] = useState(""); + const [valid, setValid] = useState(true); + const [errorMessage, setErrorMessage] = useState(""); + const [loading, setLoading] = useState(false); + const [link, setLink] = useState(""); - setEmail(email: string) { - this.setState({ email: email, valid: true, link: null, errorMessage: null }); - } + const changeEmail = function (email: string) { + setEmail(email); + setValid(true); + setLink(""); + setErrorMessage(""); + }; - async sendInvite() { - if (/[^@\s]+@[^@\s]+\.[^@\s]+/.test(this.state.email)) { - this.setState({ loading: true }); - getInviteLink("edition", this.state.email).then(ding => { - this.setState({ link: ding, loading: false }); // TODO: fix email stuff + const sendInvite = async () => { + if (/[^@\s]+@[^@\s]+\.[^@\s]+/.test(email)) { + setLoading(true); + getInviteLink("edition", email).then(ding => { + setLink(ding); + setLoading(false); + // TODO: fix email stuff }); } else { - this.setState({ valid: false, errorMessage: "Invalid email" }); + setValid(false); + setErrorMessage("Invalid email"); } - } + }; - render() { + const buttonDiv = () => { let button; - if (this.state.loading) { + if (loading) { button = ; } else { button = (
- this.sendInvite()}>Send invite + sendInvite()}>Send invite
); } + return button; + }; - let error = null; - if (this.state.errorMessage) { - error = {this.state.errorMessage}; + const errorDiv = () => { + let errorDiv = null; + if (errorMessage) { + errorDiv = {errorMessage}; } + return errorDiv; + }; - let link = null; - if (this.state.link) { - link = {this.state.link}; + const linkDiv = () => { + let linkDiv = null; + if (link) { + linkDiv = {link}; } + return linkDiv; + }; - return ( -
- - this.setEmail(e.target.value)} - /> - {button} - - {error} - {link} -
- ); - } + return ( +
+ + changeEmail(e.target.value)} + /> + {buttonDiv()} + + {errorDiv()} + {linkDiv()} +
+ ); } From 967a280fe51227b7ea5708a30e0246ee70af8967 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 27 Mar 2022 15:16:14 +0200 Subject: [PATCH 197/536] Refactor InviteUser.tsx --- .../views/UsersPage/InviteUser/InviteUser.tsx | 76 +++++++++---------- .../src/views/UsersPage/InviteUser/styles.ts | 5 +- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx index 74dcf122e..da0fd7593 100644 --- a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx +++ b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx @@ -3,6 +3,36 @@ import { getInviteLink } from "../../../utils/api/users"; import "./InviteUsers.css"; import { InviteInput, InviteButton, Loader, InviteContainer, Link, Error } from "./styles"; +function ButtonDiv(props: { loading: boolean; onClick: () => void }) { + let buttonDiv; + if (props.loading) { + buttonDiv = ; + } else { + buttonDiv = ( +
+ Send invite +
+ ); + } + return buttonDiv; +} + +function ErrorDiv(props: { errorMessage: string }) { + let errorDiv = null; + if (props.errorMessage) { + errorDiv = {props.errorMessage}; + } + return errorDiv; +} + +function LinkDiv(props: { link: string }) { + let linkDiv = null; + if (props.link) { + linkDiv = {props.link}; + } + return linkDiv; +} + export default function InviteUser() { const [email, setEmail] = useState(""); const [valid, setValid] = useState(true); @@ -20,60 +50,28 @@ export default function InviteUser() { const sendInvite = async () => { if (/[^@\s]+@[^@\s]+\.[^@\s]+/.test(email)) { setLoading(true); - getInviteLink("edition", email).then(ding => { - setLink(ding); - setLoading(false); - // TODO: fix email stuff - }); + const ding = await getInviteLink("edition", email); + setLink(ding); + setLoading(false); + // TODO: fix email stuff } else { setValid(false); setErrorMessage("Invalid email"); } }; - const buttonDiv = () => { - let button; - if (loading) { - button = ; - } else { - button = ( -
- sendInvite()}>Send invite -
- ); - } - return button; - }; - - const errorDiv = () => { - let errorDiv = null; - if (errorMessage) { - errorDiv = {errorMessage}; - } - return errorDiv; - }; - - const linkDiv = () => { - let linkDiv = null; - if (link) { - linkDiv = {link}; - } - return linkDiv; - }; - return (
changeEmail(e.target.value)} /> - {buttonDiv()} + - {errorDiv()} - {linkDiv()} + +
); } diff --git a/frontend/src/views/UsersPage/InviteUser/styles.ts b/frontend/src/views/UsersPage/InviteUser/styles.ts index 56d0bb6b3..c7737b5ba 100644 --- a/frontend/src/views/UsersPage/InviteUser/styles.ts +++ b/frontend/src/views/UsersPage/InviteUser/styles.ts @@ -4,7 +4,10 @@ export const InviteContainer = styled.div` overflow: hidden; `; -export const InviteInput = styled.input` +export const InviteInput = styled.input.attrs({ + name: "email", + placeholder: "Invite user by email", +})` height: 35px; width: 250px; font-size: 15px; From 22e892e4d8efd844710bda3f7c4af1e5c80c6ae0 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 27 Mar 2022 17:35:52 +0200 Subject: [PATCH 198/536] Pending requests template --- frontend/package.json | 1 + frontend/src/utils/api/users.ts | 68 +++++++++++++- .../views/UsersPage/InviteUser/InviteUser.tsx | 5 +- .../PendingRequests/PendingRequests.tsx | 92 +++++++++++++++++++ .../views/UsersPage/PendingRequests/index.ts | 1 + .../views/UsersPage/PendingRequests/styles.ts | 31 +++++++ frontend/src/views/UsersPage/UsersPage.tsx | 10 +- frontend/yarn.lock | 5 + 8 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx create mode 100644 frontend/src/views/UsersPage/PendingRequests/index.ts create mode 100644 frontend/src/views/UsersPage/PendingRequests/styles.ts diff --git a/frontend/package.json b/frontend/package.json index 57b1ecc33..f24443540 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "bootstrap": "5.1.3", "react": "^17.0.2", "react-bootstrap": "^2.2.1", + "react-collapsible": "^2.8.4", "react-dom": "^17.0.2", "react-icons": "^4.3.1", "react-router-bootstrap": "^0.26.1", diff --git a/frontend/src/utils/api/users.ts b/frontend/src/utils/api/users.ts index c8e53048a..41853062a 100644 --- a/frontend/src/utils/api/users.ts +++ b/frontend/src/utils/api/users.ts @@ -1,10 +1,68 @@ import axios from "axios"; import { axiosInstance } from "./api"; +export interface User { + id: Number; + name: string; + email: string; + admin: boolean; +} + +export interface Request { + id: number; + user: User; +} + +export interface GetRequestsResponse { + requests: Request[]; +} + +export async function getRequests(edition: string | undefined): Promise { + const data = { + requests: [ + { + id: 1, + user: { + id: 1, + name: "Seppe", + email: "seppe@mail.be", + admin: false, + }, + }, + { + id: 2, + user: { + id: 2, + name: "Stijn", + email: "stijn@mail.be", + admin: false, + }, + }, + ], + }; + + // eslint-disable-next-line promise/param-names + const delay = () => new Promise(res => setTimeout(res, 1000)); + await delay(); + + return data; + + // try { + // await axiosInstance + // .get(`/users/requests/?edition=${edition}`) + // .then(response => { + // return response.data; + // } + // ) + // } catch (error) { + // + // } +} + /** * Get invite link for given email and edition */ -export async function getInviteLink(edition: string, email: string): Promise { +export async function getInviteLink(edition: string | undefined, email: string): Promise { try { await axiosInstance .post(`/editions/${edition}/invites/`, { email: email }) @@ -20,3 +78,11 @@ export async function getInviteLink(edition: string, email: string): Promise { if (/[^@\s]+@[^@\s]+\.[^@\s]+/.test(email)) { setLoading(true); - const ding = await getInviteLink("edition", email); + const ding = await getInviteLink("props.edition", email); setLink(ding); setLoading(false); // TODO: fix email stuff @@ -62,6 +62,7 @@ export default function InviteUser() { return (
+
{props.edition}
Requests; +} + +function AcceptReject(props: { request_id: number }) { + return ( +
+ acceptRequest(props.request_id)}>Accept + rejectRequest(props.request_id)}>Reject +
+ ); +} + +function RequestItem(props: { request: Request }) { + return ( + + {props.request.user.name} + {props.request.user.email} + + + + + ); +} + +function RequestsList(props: { requests: Request[]; loading: boolean }) { + if (props.loading) { + return ; + } else if (props.requests.length === 0) { + return
No requests
; + } + + const body = ( + + {props.requests.map(request => ( + + ))} + + ); + props.requests.map(request => ); + + return ( + + + + Name + Email + Accept/Reject + + + {body} + + ); +} + +export default function PendingRequests(props: { edition: string | undefined }) { + const [requests, setRequests] = useState([]); + const [gettingRequests, setGettingRequests] = useState(true); + + useEffect(() => { + getRequests(props.edition) + .then(response => { + setRequests(response.requests); + setGettingRequests(false); + }) + .catch(function (error: any) { + console.log(error); + setGettingRequests(false); + }); + }); + + return ( + + }> + + + + ); +} diff --git a/frontend/src/views/UsersPage/PendingRequests/index.ts b/frontend/src/views/UsersPage/PendingRequests/index.ts new file mode 100644 index 000000000..6e87caa6b --- /dev/null +++ b/frontend/src/views/UsersPage/PendingRequests/index.ts @@ -0,0 +1 @@ +export { default as PendingRequests } from "./PendingRequests"; diff --git a/frontend/src/views/UsersPage/PendingRequests/styles.ts b/frontend/src/views/UsersPage/PendingRequests/styles.ts new file mode 100644 index 000000000..fe3163e33 --- /dev/null +++ b/frontend/src/views/UsersPage/PendingRequests/styles.ts @@ -0,0 +1,31 @@ +import styled from "styled-components"; +import { Table } from "react-bootstrap"; + +export const RequestHeader = styled.div` + background-color: var(--osoc_red); + padding-bottom: 3px; + padding-left: 3px; +`; + +export const RequestsTable = styled(Table)``; + +export const PendingRequestsContainer = styled.div` + width: 50%; + min-width: 600px; + margin: 10px auto auto; +`; + +export const AcceptButton = styled.button` + background-color: var(--osoc_green); + padding-bottom: 3px; + padding-left: 3px; + padding-right: 3px; +`; + +export const RejectButton = styled.button` + background-color: var(--osoc_red); + margin-left: 3px; + padding-bottom: 3px; + padding-left: 3px; + padding-right: 3px; +`; diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index ed9a12a2b..fab2469bc 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -1,8 +1,16 @@ import React from "react"; import { InviteUser } from "./InviteUser"; +import { PendingRequests } from "./PendingRequests"; +import { useParams } from "react-router-dom"; function UsersPage() { - return ; + const params = useParams(); + return ( +
+ + +
+ ); } export default UsersPage; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 9cb056679..3e8147eba 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -7588,6 +7588,11 @@ react-bootstrap@^2.2.1: uncontrollable "^7.2.1" warning "^4.0.3" +react-collapsible@^2.8.4: + version "2.8.4" + resolved "https://registry.yarnpkg.com/react-collapsible/-/react-collapsible-2.8.4.tgz#319ff7471138c4381ce0afa3ac308ccde7f4e09f" + 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" From a7ba10520bf4925d5adffc2ddb14ae13aa258605 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 27 Mar 2022 21:17:44 +0200 Subject: [PATCH 199/536] Filter Pending Requests --- .../PendingRequests/PendingRequests.tsx | 76 +++++++++++++++---- .../views/UsersPage/PendingRequests/styles.ts | 23 +++++- 2 files changed, 81 insertions(+), 18 deletions(-) diff --git a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx index 4950eb569..701942437 100644 --- a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx +++ b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx @@ -1,18 +1,27 @@ import React, { useEffect, useState } from "react"; import Collapsible from "react-collapsible"; import { - RequestHeader, + RequestHeaderTitle, RequestsTable, PendingRequestsContainer, AcceptButton, RejectButton, + SpinnerContainer, + SearchInput, } from "./styles"; import { acceptRequest, getRequests, rejectRequest, Request } from "../../../utils/api/users"; import { Spinner } from "react-bootstrap"; -function RequestsHeader() { - // TODO: Search field when out-folded - return Requests; +function RequestHeader(props: { open: boolean }) { + return Requests {props.open ? "opened" : "closed"}; +} + +function RequestFilter(props: { + search: boolean; + searchTerm: string; + filter: (key: string) => void; +}) { + return props.filter(e.target.value)} />; } function AcceptReject(props: { request_id: number }) { @@ -38,7 +47,11 @@ function RequestItem(props: { request: Request }) { function RequestsList(props: { requests: Request[]; loading: boolean }) { if (props.loading) { - return ; + return ( + + + + ); } else if (props.requests.length === 0) { return
No requests
; } @@ -50,7 +63,6 @@ function RequestsList(props: { requests: Request[]; loading: boolean }) { ))} ); - props.requests.map(request => ); return ( @@ -67,24 +79,56 @@ function RequestsList(props: { requests: Request[]; loading: boolean }) { } export default function PendingRequests(props: { edition: string | undefined }) { + const [allRequests, setAllRequests] = useState([]); const [requests, setRequests] = useState([]); const [gettingRequests, setGettingRequests] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [gotData, setGotData] = useState(false); + const [open, setOpen] = useState(false); useEffect(() => { - getRequests(props.edition) - .then(response => { - setRequests(response.requests); - setGettingRequests(false); - }) - .catch(function (error: any) { - console.log(error); - setGettingRequests(false); - }); + if (!gotData) { + getRequests(props.edition) + .then(response => { + setRequests(response.requests); + setAllRequests(response.requests); + setGettingRequests(false); + setGotData(true); + }) + .catch(function (error: any) { + console.log(error); + setGettingRequests(false); + }); + } }); + const filter = (word: string) => { + setSearchTerm(word); + const newRequests: Request[] = []; + for (const request of allRequests) { + if ( + request.user.name.toUpperCase().includes(word.toUpperCase()) || + request.user.email.toUpperCase().includes(word.toUpperCase()) + ) { + newRequests.push(request); + } + } + setRequests(newRequests); + }; + + // @ts-ignore return ( - }> + } + onOpen={() => setOpen(true)} + onClose={() => setOpen(false)} + > + 0} + searchTerm={searchTerm} + filter={word => filter(word)} + /> diff --git a/frontend/src/views/UsersPage/PendingRequests/styles.ts b/frontend/src/views/UsersPage/PendingRequests/styles.ts index fe3163e33..17e81b40a 100644 --- a/frontend/src/views/UsersPage/PendingRequests/styles.ts +++ b/frontend/src/views/UsersPage/PendingRequests/styles.ts @@ -1,10 +1,22 @@ import styled from "styled-components"; import { Table } from "react-bootstrap"; -export const RequestHeader = styled.div` - background-color: var(--osoc_red); +export const RequestHeaderTitle = styled.div` padding-bottom: 3px; padding-left: 3px; + width: 100px; + font-size: 25px; +`; + +export const SearchInput = styled.input.attrs({ + placeholder: "Search", +})` + margin: 3px; + height: 20px; + width: 150px; + font-size: 15px; + border-radius: 5px; + border-width: 0; `; export const RequestsTable = styled(Table)``; @@ -29,3 +41,10 @@ export const RejectButton = styled.button` padding-left: 3px; padding-right: 3px; `; + +export const SpinnerContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + margin: 20px; +`; From 765759220b288ff640dce4b2b083613fbfc69de8 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sat, 2 Apr 2022 14:46:12 +0200 Subject: [PATCH 200/536] Coaches list --- frontend/package.json | 1 + frontend/src/utils/api/users/coaches.ts | 38 ++++ .../utils/api/{users.ts => users/requests.ts} | 40 +---- frontend/src/utils/api/users/users.ts | 29 +++ .../src/views/UsersPage/Coaches/Coaches.tsx | 167 ++++++++++++++++++ frontend/src/views/UsersPage/Coaches/index.ts | 1 + .../src/views/UsersPage/Coaches/styles.ts | 39 ++++ .../views/UsersPage/InviteUser/InviteUser.tsx | 7 +- .../PendingRequests/PendingRequests.tsx | 32 ++-- .../views/UsersPage/PendingRequests/styles.ts | 8 +- frontend/src/views/UsersPage/UsersPage.tsx | 19 +- frontend/yarn.lock | 5 + 12 files changed, 321 insertions(+), 65 deletions(-) create mode 100644 frontend/src/utils/api/users/coaches.ts rename frontend/src/utils/api/{users.ts => users/requests.ts} (53%) create mode 100644 frontend/src/utils/api/users/users.ts create mode 100644 frontend/src/views/UsersPage/Coaches/Coaches.tsx create mode 100644 frontend/src/views/UsersPage/Coaches/index.ts create mode 100644 frontend/src/views/UsersPage/Coaches/styles.ts diff --git a/frontend/package.json b/frontend/package.json index f24443540..006725f83 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "react-router-dom": "^6.2.1", "react-scripts": "^5.0.0", "react-social-login-buttons": "^3.6.0", + "reactjs-popup": "^2.0.5", "styled-components": "^5.3.3", "typescript": "^4.4.2", "web-vitals": "^2.1.0" diff --git a/frontend/src/utils/api/users/coaches.ts b/frontend/src/utils/api/users/coaches.ts new file mode 100644 index 000000000..880e6d3e7 --- /dev/null +++ b/frontend/src/utils/api/users/coaches.ts @@ -0,0 +1,38 @@ +import { User } from "./users"; + +export interface GetCoachesResponse { + coaches: User[]; +} + +export async function getCoaches(edition: string): Promise { + const data = { + coaches: [ + { + id: 3, + name: "Bert", + email: "bert@mail.be", + admin: false, + }, + { + id: 4, + name: "Tiebe", + email: "tiebe@mail.be", + admin: false, + }, + ], + }; + + // eslint-disable-next-line promise/param-names + const delay = () => new Promise(res => setTimeout(res, 100)); + await delay(); + + return data; +} + +export async function removeCoachFromEdition(userId: number, edition: string) { + alert("remove " + userId + " from " + edition); +} + +export async function removeCoachFromAllEditions(userId: Number) { + alert("remove " + userId + " from all editions"); +} diff --git a/frontend/src/utils/api/users.ts b/frontend/src/utils/api/users/requests.ts similarity index 53% rename from frontend/src/utils/api/users.ts rename to frontend/src/utils/api/users/requests.ts index 41853062a..a1716e187 100644 --- a/frontend/src/utils/api/users.ts +++ b/frontend/src/utils/api/users/requests.ts @@ -1,12 +1,4 @@ -import axios from "axios"; -import { axiosInstance } from "./api"; - -export interface User { - id: Number; - name: string; - email: string; - admin: boolean; -} +import { User } from "./users"; export interface Request { id: number; @@ -17,7 +9,7 @@ export interface GetRequestsResponse { requests: Request[]; } -export async function getRequests(edition: string | undefined): Promise { +export async function getRequests(edition: string): Promise { const data = { requests: [ { @@ -59,30 +51,10 @@ export async function getRequests(edition: string | undefined): Promise { - try { - await axiosInstance - .post(`/editions/${edition}/invites/`, { email: email }) - .then(response => { - return response.data.mailTo; - }); - } catch (error) { - if (axios.isAxiosError(error)) { - return error.message; - } else { - throw error; - } - } - return ""; -} - -export async function acceptRequest(requestId: Number) { - alert("Accept"); +export async function acceptRequest(requestId: number) { + alert("Accept " + requestId); } -export async function rejectRequest(requestId: Number) { - alert("Reject"); +export async function rejectRequest(requestId: number) { + alert("Reject " + requestId); } diff --git a/frontend/src/utils/api/users/users.ts b/frontend/src/utils/api/users/users.ts new file mode 100644 index 000000000..fdc5daa8c --- /dev/null +++ b/frontend/src/utils/api/users/users.ts @@ -0,0 +1,29 @@ +import axios from "axios"; +import { axiosInstance } from "../api"; + +export interface User { + id: number; + name: string; + email: string; + admin: boolean; +} + +/** + * Get invite link for given email and edition + */ +export async function getInviteLink(edition: string | undefined, email: string): Promise { + try { + await axiosInstance + .post(`/editions/${edition}/invites/`, { email: email }) + .then(response => { + return response.data.mailTo; + }); + } catch (error) { + if (axios.isAxiosError(error)) { + return error.message; + } else { + throw error; + } + } + return ""; +} diff --git a/frontend/src/views/UsersPage/Coaches/Coaches.tsx b/frontend/src/views/UsersPage/Coaches/Coaches.tsx new file mode 100644 index 000000000..d5ec75481 --- /dev/null +++ b/frontend/src/views/UsersPage/Coaches/Coaches.tsx @@ -0,0 +1,167 @@ +import React, { useEffect, useState } from "react"; +import { CoachesTitle, CoachesContainer, CoachesTable, ModalContent } from "./styles"; +import { User } from "../../../utils/api/users/users"; +import { SearchInput, SpinnerContainer } from "../PendingRequests/styles"; +import { Button, Modal, Spinner } from "react-bootstrap"; +import { + getCoaches, + removeCoachFromAllEditions, + removeCoachFromEdition, +} from "../../../utils/api/users/coaches"; + +function CoachesHeader() { + return Coaches; +} + +function CoachFilter(props: { + search: boolean; + searchTerm: string; + filter: (key: string) => void; +}) { + return props.filter(e.target.value)} />; +} + +function RemoveCoach(props: { coach: User; edition: string }) { + const [show, setShow] = useState(false); + + const handleClose = () => setShow(false); + const handleShow = () => setShow(true); + + return ( + <> + + + + + + Remove Coach + + +

{props.coach.name}

+ {props.coach.email} +
+ + + + + +
+
+ + ); +} + +function CoachItem(props: { coach: User; edition: string }) { + return ( + + {props.coach.name} + {props.coach.email} + + + + + ); +} + +function CoachesList(props: { coaches: User[]; loading: boolean; edition: string }) { + if (props.loading) { + return ( + + + + ); + } else if (props.coaches.length === 0) { + return
No coaches for this edition
; + } + + const body = ( + + {props.coaches.map(coach => ( + + ))} + + ); + + return ( + + + + Name + Email + Remove from edition + + + {body} + + ); +} + +export default function Coaches(props: { edition: string }) { + const [allCoaches, setAllCoaches] = useState([]); + const [coaches, setCoaches] = useState([]); + const [gettingCoaches, setGettingCoaches] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [gotData, setGotData] = useState(false); + + useEffect(() => { + if (!gotData) { + getCoaches(props.edition) + .then(response => { + setCoaches(response.coaches); + setAllCoaches(response.coaches); + setGettingCoaches(false); + setGotData(true); + }) + .catch(function (error: any) { + console.log(error); + setGettingCoaches(false); + }); + } + }); + + const filter = (word: string) => { + setSearchTerm(word); + const newCoaches: User[] = []; + for (const coach of allCoaches) { + if ( + coach.name.toUpperCase().includes(word.toUpperCase()) || + coach.email.toUpperCase().includes(word.toUpperCase()) + ) { + newCoaches.push(coach); + } + } + setCoaches(newCoaches); + }; + + return ( + + + 0} + searchTerm={searchTerm} + filter={word => filter(word)} + /> + + + ); +} diff --git a/frontend/src/views/UsersPage/Coaches/index.ts b/frontend/src/views/UsersPage/Coaches/index.ts new file mode 100644 index 000000000..ef8ea8035 --- /dev/null +++ b/frontend/src/views/UsersPage/Coaches/index.ts @@ -0,0 +1 @@ +export { default as Coaches } from "./Coaches"; diff --git a/frontend/src/views/UsersPage/Coaches/styles.ts b/frontend/src/views/UsersPage/Coaches/styles.ts new file mode 100644 index 000000000..1f8970bd5 --- /dev/null +++ b/frontend/src/views/UsersPage/Coaches/styles.ts @@ -0,0 +1,39 @@ +import styled from "styled-components"; +import { Table } from "react-bootstrap"; + +export const CoachesContainer = styled.div` + width: 50%; + min-width: 600px; + margin: 10px auto auto; +`; + +export const CoachesTitle = styled.div` + padding-bottom: 3px; + padding-left: 3px; + width: 100px; + font-size: 25px; +`; + +export const RemoveFromEditionButton = styled.button` + background-color: var(--osoc_red); + margin-left: 3px; + padding-bottom: 3px; + padding-left: 3px; + padding-right: 3px; +`; + +export const CoachesTable = styled(Table)``; + +export const PopupDiv = styled.div` + background-color: var(--osoc_red); + width: 200px; + height: 100px; + position: absolute; + right: 0; + top: 0; +`; + +export const ModalContent = styled.div` + border: 3px solid var(--osoc_red); + background-color: var(--osoc_blue); +`; diff --git a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx index e0e4b7050..22b4339b5 100644 --- a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx +++ b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { getInviteLink } from "../../../utils/api/users"; +import { getInviteLink } from "../../../utils/api/users/users"; import "./InviteUsers.css"; import { InviteInput, InviteButton, Loader, InviteContainer, Link, Error } from "./styles"; @@ -33,7 +33,7 @@ function LinkDiv(props: { link: string }) { return linkDiv; } -export default function InviteUser(props: { edition: string | undefined }) { +export default function InviteUser(props: { edition: string }) { const [email, setEmail] = useState(""); const [valid, setValid] = useState(true); const [errorMessage, setErrorMessage] = useState(""); @@ -50,7 +50,7 @@ export default function InviteUser(props: { edition: string | undefined }) { const sendInvite = async () => { if (/[^@\s]+@[^@\s]+\.[^@\s]+/.test(email)) { setLoading(true); - const ding = await getInviteLink("props.edition", email); + const ding = await getInviteLink(props.edition, email); setLink(ding); setLoading(false); // TODO: fix email stuff @@ -62,7 +62,6 @@ export default function InviteUser(props: { edition: string | undefined }) { return (
-
{props.edition}
Requests {props.open ? "opened" : "closed"}; + return Requests; } -function RequestFilter(props: { - search: boolean; - searchTerm: string; - filter: (key: string) => void; -}) { +function RequestFilter(props: { searchTerm: string; filter: (key: string) => void }) { return props.filter(e.target.value)} />; } -function AcceptReject(props: { request_id: number }) { +function AcceptReject(props: { requestId: number }) { return (
- acceptRequest(props.request_id)}>Accept - rejectRequest(props.request_id)}>Reject + acceptRequest(props.requestId)}>Accept + rejectRequest(props.requestId)}>Reject
); } @@ -39,7 +40,7 @@ function RequestItem(props: { request: Request }) { {props.request.user.name} {props.request.user.email} - + ); @@ -78,7 +79,7 @@ function RequestsList(props: { requests: Request[]; loading: boolean }) { ); } -export default function PendingRequests(props: { edition: string | undefined }) { +export default function PendingRequests(props: { edition: string }) { const [allRequests, setAllRequests] = useState([]); const [requests, setRequests] = useState([]); const [gettingRequests, setGettingRequests] = useState(true); @@ -116,7 +117,6 @@ export default function PendingRequests(props: { edition: string | undefined }) setRequests(newRequests); }; - // @ts-ignore return ( setOpen(true)} onClose={() => setOpen(false)} > - 0} - searchTerm={searchTerm} - filter={word => filter(word)} - /> + filter(word)} /> diff --git a/frontend/src/views/UsersPage/PendingRequests/styles.ts b/frontend/src/views/UsersPage/PendingRequests/styles.ts index 17e81b40a..6c004c5ff 100644 --- a/frontend/src/views/UsersPage/PendingRequests/styles.ts +++ b/frontend/src/views/UsersPage/PendingRequests/styles.ts @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { Table } from "react-bootstrap"; +import { Table, Button } from "react-bootstrap"; export const RequestHeaderTitle = styled.div` padding-bottom: 3px; @@ -27,15 +27,17 @@ export const PendingRequestsContainer = styled.div` margin: 10px auto auto; `; -export const AcceptButton = styled.button` +export const AcceptButton = styled(Button)` background-color: var(--osoc_green); + color: black; padding-bottom: 3px; padding-left: 3px; padding-right: 3px; `; -export const RejectButton = styled.button` +export const RejectButton = styled(Button)` background-color: var(--osoc_red); + color: black; margin-left: 3px; padding-bottom: 3px; padding-left: 3px; diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index fab2469bc..3740965ff 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -1,16 +1,23 @@ import React from "react"; import { InviteUser } from "./InviteUser"; import { PendingRequests } from "./PendingRequests"; +import { Coaches } from "./Coaches"; import { useParams } from "react-router-dom"; function UsersPage() { const params = useParams(); - return ( -
- - -
- ); + + if (params.edition === undefined) { + return
Error
; + } else { + return ( +
+ + + +
+ ); + } } export default UsersPage; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 3e8147eba..731653d05 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -7772,6 +7772,11 @@ react@^17.0.2: loose-envify "^1.1.0" object-assign "^4.1.1" +reactjs-popup@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/reactjs-popup/-/reactjs-popup-2.0.5.tgz#588a74966bb126699429d739948e3448d7771eac" + 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" From d31fc024347c9901963deeeabda64fa4a5edc52f Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sat, 2 Apr 2022 18:03:43 +0200 Subject: [PATCH 201/536] Manage admins --- frontend/package.json | 1 + frontend/src/utils/api/users/admins.ts | 44 ++++ frontend/src/utils/api/users/coaches.ts | 2 +- frontend/src/utils/api/users/users.ts | 61 ++++- .../src/views/AdminsPage/Admins/Admins.tsx | 240 ++++++++++++++++++ frontend/src/views/AdminsPage/Admins/index.ts | 1 + .../src/views/AdminsPage/Admins/styles.ts | 25 ++ frontend/src/views/AdminsPage/AdminsPage.tsx | 13 + .../src/views/UsersPage/Coaches/Coaches.tsx | 2 +- .../views/UsersPage/InviteUser/InviteUser.tsx | 6 +- .../src/views/UsersPage/InviteUser/styles.ts | 17 +- .../PendingRequests/PendingRequests.tsx | 2 +- frontend/src/views/UsersPage/UsersPage.tsx | 14 +- frontend/src/views/UsersPage/styles.ts | 18 ++ frontend/yarn.lock | 76 +++++- 15 files changed, 495 insertions(+), 27 deletions(-) create mode 100644 frontend/src/utils/api/users/admins.ts create mode 100644 frontend/src/views/AdminsPage/Admins/Admins.tsx create mode 100644 frontend/src/views/AdminsPage/Admins/index.ts create mode 100644 frontend/src/views/AdminsPage/Admins/styles.ts create mode 100644 frontend/src/views/AdminsPage/AdminsPage.tsx create mode 100644 frontend/src/views/UsersPage/styles.ts diff --git a/frontend/package.json b/frontend/package.json index 006725f83..28bf31665 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "bootstrap": "5.1.3", "react": "^17.0.2", "react-bootstrap": "^2.2.1", + "react-bootstrap-typeahead": "^6.0.0-alpha.11", "react-collapsible": "^2.8.4", "react-dom": "^17.0.2", "react-icons": "^4.3.1", diff --git a/frontend/src/utils/api/users/admins.ts b/frontend/src/utils/api/users/admins.ts new file mode 100644 index 000000000..437e1eb56 --- /dev/null +++ b/frontend/src/utils/api/users/admins.ts @@ -0,0 +1,44 @@ +import { User } from "./users"; + +export interface GetAdminsResponse { + admins: User[]; +} + +export async function getAdmins(): Promise { + const data = { + admins: [ + { + id: 5, + name: "Ward", + email: "ward@mail.be", + admin: true, + }, + { + id: 6, + name: "Francis", + email: "francis@mail.be", + admin: true, + }, + { + id: 7, + name: "Clement", + email: "clement@mail.be", + admin: true, + }, + ], + }; + + // eslint-disable-next-line promise/param-names + const delay = () => new Promise(res => setTimeout(res, 100)); + await delay(); + + return data; +} + +export async function addAdmin(userId: number) { + alert("add " + userId + " as admin"); +} + +export async function removeAdmin(userId: number) { + alert("remove " + userId + " as admin"); +} diff --git a/frontend/src/utils/api/users/coaches.ts b/frontend/src/utils/api/users/coaches.ts index 880e6d3e7..43fc40bda 100644 --- a/frontend/src/utils/api/users/coaches.ts +++ b/frontend/src/utils/api/users/coaches.ts @@ -33,6 +33,6 @@ export async function removeCoachFromEdition(userId: number, edition: string) { alert("remove " + userId + " from " + edition); } -export async function removeCoachFromAllEditions(userId: Number) { +export async function removeCoachFromAllEditions(userId: number) { alert("remove " + userId + " from all editions"); } diff --git a/frontend/src/utils/api/users/users.ts b/frontend/src/utils/api/users/users.ts index fdc5daa8c..5a3bdfb14 100644 --- a/frontend/src/utils/api/users/users.ts +++ b/frontend/src/utils/api/users/users.ts @@ -11,7 +11,7 @@ export interface User { /** * Get invite link for given email and edition */ -export async function getInviteLink(edition: string | undefined, email: string): Promise { +export async function getInviteLink(edition: string, email: string): Promise { try { await axiosInstance .post(`/editions/${edition}/invites/`, { email: email }) @@ -27,3 +27,62 @@ export async function getInviteLink(edition: string | undefined, email: string): } return ""; } + +export interface GetUsersResponse { + users: User[]; +} + +export async function getUsers(): Promise { + const data = { + users: [ + { + id: 1, + name: "Seppe", + email: "seppe@mail.be", + admin: false, + }, + { + id: 2, + name: "Stijn", + email: "stijn@mail.be", + admin: false, + }, + { + id: 3, + name: "Bert", + email: "bert@mail.be", + admin: false, + }, + { + id: 4, + name: "Tiebe", + email: "tiebe@mail.be", + admin: false, + }, + { + id: 5, + name: "Ward", + email: "ward@mail.be", + admin: true, + }, + { + id: 6, + name: "Francis", + email: "francis@mail.be", + admin: true, + }, + { + id: 7, + name: "Clement", + email: "clement@mail.be", + admin: true, + }, + ], + }; + + // eslint-disable-next-line promise/param-names + const delay = () => new Promise(res => setTimeout(res, 100)); + await delay(); + + return data; +} diff --git a/frontend/src/views/AdminsPage/Admins/Admins.tsx b/frontend/src/views/AdminsPage/Admins/Admins.tsx new file mode 100644 index 000000000..39df4af38 --- /dev/null +++ b/frontend/src/views/AdminsPage/Admins/Admins.tsx @@ -0,0 +1,240 @@ +import React, { useEffect, useState } from "react"; +import { AdminsContainer, AdminsTable, ModalContent, AddAdminButton, Warning } from "./styles"; +import { getUsers, User } from "../../../utils/api/users/users"; +import { Button, Modal, Spinner } from "react-bootstrap"; +import { addAdmin, getAdmins, removeAdmin } from "../../../utils/api/users/admins"; +import { SearchInput, SpinnerContainer } from "../../UsersPage/PendingRequests/styles"; +import { Typeahead } from "react-bootstrap-typeahead"; + +function AdminFilter(props: { + search: boolean; + searchTerm: string; + filter: (key: string) => void; +}) { + return props.filter(e.target.value)} />; +} + +function AddWarning(props: { name: string | undefined }) { + if (props.name !== undefined) { + return Warning: {props.name} will be able to edit/delete all data. ; + } + return null; +} + +function AddAdmin(props: { users: User[] }) { + const [show, setShow] = useState(false); + const [selected, setSelected] = useState(undefined); + + const handleClose = () => { + setSelected(undefined); + setShow(false); + }; + const handleShow = () => setShow(true); + + return ( + <> + + Add admin + + + + + + Add Admin + + + { + // @ts-ignore + setSelected(selected[0]); + }} + options={props.users} + labelKey={user => `${user.name} (${user.email})`} + filterBy={["email", "name"]} + /> + + + + + + + + + + ); +} + +function RemoveAdmin(props: { admin: User }) { + const [show, setShow] = useState(false); + + const handleClose = () => setShow(false); + const handleShow = () => setShow(true); + + return ( + <> + + + + + + Remove Admin + + +

{props.admin.name}

+

{props.admin.email}

+

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

+
+ + + + + +
+
+ + ); +} + +function AdminItem(props: { admin: User }) { + return ( + + {props.admin.name} + {props.admin.email} + + + + + ); +} + +function AdminsList(props: { admins: User[]; loading: boolean }) { + if (props.loading) { + return ( + + + + ); + } else if (props.admins.length === 0) { + return
No admins? #rip
; + } + + const body = ( + + {props.admins.map(admin => ( + + ))} + + ); + + return ( + + + + Name + Email + Remove + + + {body} + + ); +} + +export default function Admins() { + const [allAdmins, setAllAdmins] = useState([]); + const [admins, setAdmins] = useState([]); + const [users, setUsers] = useState([]); + const [gettingAdmins, setGettingAdmins] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [gotData, setGotData] = useState(false); + + useEffect(() => { + if (!gotData) { + getAdmins() + .then(response => { + setAdmins(response.admins); + setAllAdmins(response.admins); + setGettingAdmins(false); + setGotData(true); + }) + .catch(function (error: any) { + console.log(error); + setGettingAdmins(false); + }); + getUsers() + .then(response => { + const users = []; + for (const user of response.users) { + if (!allAdmins.some(e => e.id === user.id)) { + users.push(user); + } + } + setUsers(users); + }) + .catch(function (error: any) { + console.log(error); + }); + } + }); + + const filter = (word: string) => { + setSearchTerm(word); + const newCoaches: User[] = []; + for (const admin of allAdmins) { + if ( + admin.name.toUpperCase().includes(word.toUpperCase()) || + admin.email.toUpperCase().includes(word.toUpperCase()) + ) { + newCoaches.push(admin); + } + } + setAdmins(newCoaches); + }; + + return ( + + 0} + searchTerm={searchTerm} + filter={word => filter(word)} + /> + + + + ); +} diff --git a/frontend/src/views/AdminsPage/Admins/index.ts b/frontend/src/views/AdminsPage/Admins/index.ts new file mode 100644 index 000000000..549843890 --- /dev/null +++ b/frontend/src/views/AdminsPage/Admins/index.ts @@ -0,0 +1 @@ +export { default as Admins } from "./Admins"; diff --git a/frontend/src/views/AdminsPage/Admins/styles.ts b/frontend/src/views/AdminsPage/Admins/styles.ts new file mode 100644 index 000000000..75de16bb6 --- /dev/null +++ b/frontend/src/views/AdminsPage/Admins/styles.ts @@ -0,0 +1,25 @@ +import styled from "styled-components"; +import { Button, Table } from "react-bootstrap"; + +export const AdminsContainer = styled.div` + width: 50%; + min-width: 600px; + margin: 10px auto auto; +`; + +export const AdminsTable = styled(Table)``; + +export const ModalContent = styled.div` + border: 3px solid var(--osoc_green); + background-color: var(--osoc_blue); +`; + +export const AddAdminButton = styled(Button).attrs({ + size: "sm", +})` + float: right; +`; + +export const Warning = styled.div` + color: var(--osoc_red); +`; diff --git a/frontend/src/views/AdminsPage/AdminsPage.tsx b/frontend/src/views/AdminsPage/AdminsPage.tsx new file mode 100644 index 000000000..62ee215e0 --- /dev/null +++ b/frontend/src/views/AdminsPage/AdminsPage.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { Admins } from "./Admins"; + +function AdminsPage() { + return ( +
+

Manage admins

+ +
+ ); +} + +export default AdminsPage; diff --git a/frontend/src/views/UsersPage/Coaches/Coaches.tsx b/frontend/src/views/UsersPage/Coaches/Coaches.tsx index d5ec75481..38cc5f9fb 100644 --- a/frontend/src/views/UsersPage/Coaches/Coaches.tsx +++ b/frontend/src/views/UsersPage/Coaches/Coaches.tsx @@ -97,7 +97,7 @@ function CoachesList(props: { coaches: User[]; loading: boolean; edition: string const body = ( {props.coaches.map(coach => ( - + ))} ); diff --git a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx index 22b4339b5..ced2c13f9 100644 --- a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx +++ b/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx @@ -8,11 +8,7 @@ function ButtonDiv(props: { loading: boolean; onClick: () => void }) { if (props.loading) { buttonDiv = ; } else { - buttonDiv = ( -
- Send invite -
- ); + buttonDiv = Send invite; } return buttonDiv; } diff --git a/frontend/src/views/UsersPage/InviteUser/styles.ts b/frontend/src/views/UsersPage/InviteUser/styles.ts index c7737b5ba..9d9e8a06c 100644 --- a/frontend/src/views/UsersPage/InviteUser/styles.ts +++ b/frontend/src/views/UsersPage/InviteUser/styles.ts @@ -1,16 +1,17 @@ import styled, { keyframes } from "styled-components"; +import { Button } from "react-bootstrap"; export const InviteContainer = styled.div` - overflow: hidden; + clear: both; `; export const InviteInput = styled.input.attrs({ name: "email", placeholder: "Invite user by email", })` - height: 35px; - width: 250px; - font-size: 15px; + height: 30px; + width: 200px; + font-size: 13px; margin-top: 10px; margin-left: 10px; text-align: center; @@ -19,14 +20,12 @@ export const InviteInput = styled.input.attrs({ float: left; `; -export const InviteButton = styled.button` - width: 90px; - height: 35px; +export const InviteButton = styled(Button).attrs({ + size: "sm", +})` cursor: pointer; background: var(--osoc_green); color: white; - border: none; - border-radius: 5px; margin-left: 7px; margin-top: 10px; `; diff --git a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx index 069713fbc..3b02b0766 100644 --- a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx +++ b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx @@ -60,7 +60,7 @@ function RequestsList(props: { requests: Request[]; loading: boolean }) { const body = ( {props.requests.map(request => ( - + ))} ); diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index 3740965ff..bd267f371 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -2,20 +2,28 @@ import React from "react"; import { InviteUser } from "./InviteUser"; import { PendingRequests } from "./PendingRequests"; import { Coaches } from "./Coaches"; -import { useParams } from "react-router-dom"; +import { useParams, useNavigate } from "react-router-dom"; +import { UsersPageDiv, AdminsButton, UsersHeader } from "./styles"; function UsersPage() { const params = useParams(); + const navigate = useNavigate(); if (params.edition === undefined) { return
Error
; } else { return ( -
+ +
+ +

Manage coaches from {params.edition}

+
+ navigate("/admins")}>Edit Admins +
-
+ ); } } diff --git a/frontend/src/views/UsersPage/styles.ts b/frontend/src/views/UsersPage/styles.ts new file mode 100644 index 000000000..d7adfe0ae --- /dev/null +++ b/frontend/src/views/UsersPage/styles.ts @@ -0,0 +1,18 @@ +import styled from "styled-components"; +import { Button } from "react-bootstrap"; + +export const UsersPageDiv = styled.div``; + +export const AdminsButton = styled(Button)` + background-color: var(--osoc_green); + margin-right: 10px; + margin-top: 10px; + float: right; +`; + +export const UsersHeader = styled.div` + padding-left: 10px; + margin-top: 10px; + float: left; + display: inline-block; +`; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 731653d05..f85b4a78b 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1024,7 +1024,7 @@ 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.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": +"@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== @@ -1454,7 +1454,7 @@ schema-utils "^3.0.0" source-map "^0.7.3" -"@popperjs/core@^2.10.1": +"@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" integrity sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg== @@ -1466,6 +1466,13 @@ 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" + 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" @@ -3138,7 +3145,7 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== -classnames@^2.3.1: +classnames@^2.2.0, classnames@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== @@ -3274,6 +3281,11 @@ compression@^1.7.4: safe-buffer "5.1.2" vary "~1.1.2" +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" + 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" @@ -5216,7 +5228,7 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" -invariant@^2.2.4: +invariant@^2.2.1, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -7465,7 +7477,7 @@ prop-types-extra@^1.1.0: react-is "^16.3.2" warning "^4.0.0" -prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.5.8, 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" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -7566,6 +7578,24 @@ react-app-polyfill@^3.0.0: regenerator-runtime "^0.13.9" whatwg-fetch "^3.6.2" +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" + integrity sha512-yHBPsdkAdvvLpkq6wWei55qt4REdbRnC+1wVxkBSBeTG4Z6lkKKSGx6w4kY9YmkyyVcbmmfwUSXhCWY/M+TzCg== + dependencies: + "@babel/runtime" "^7.14.6" + "@popperjs/core" "^2.10.2" + "@restart/hooks" "^0.4.0" + classnames "^2.2.0" + fast-deep-equal "^3.1.1" + invariant "^2.2.1" + lodash.debounce "^4.0.8" + prop-types "^15.5.8" + react-overlays "^5.1.0" + react-popper "^2.2.5" + 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" @@ -7637,6 +7667,11 @@ react-error-overlay@^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-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" + 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" @@ -7657,6 +7692,28 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" 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" + integrity sha512-eCN2s2/+GVZzpnId4XVWtvDPYYBD2EtOGP74hE+8yDskPzFy9+pV1H3ZZihxuRdEbQzzacySaaDkR7xE0ydl4Q== + dependencies: + "@babel/runtime" "^7.13.8" + "@popperjs/core" "^2.8.6" + "@restart/hooks" "^0.3.26" + "@types/warning" "^3.0.0" + dom-helpers "^5.2.0" + prop-types "^15.7.2" + uncontrollable "^7.2.1" + warning "^4.0.3" + +react-popper@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.5.tgz#1214ef3cec86330a171671a4fbcbeeb65ee58e96" + integrity sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw== + dependencies: + react-fast-compare "^3.0.1" + warning "^4.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" @@ -8120,6 +8177,13 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.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" + 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" @@ -9028,7 +9092,7 @@ walker@^1.0.7: dependencies: makeerror "1.0.12" -warning@^4.0.0, warning@^4.0.3: +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" integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== From 58c36da3a4b75aad946a20ccd9fb983ff28d8c29 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sat, 2 Apr 2022 20:26:13 +0200 Subject: [PATCH 202/536] Small fixes --- frontend/src/utils/api/users/admins.ts | 4 ++ .../src/views/AdminsPage/Admins/Admins.tsx | 39 +++++++++++++------ .../src/views/AdminsPage/Admins/styles.ts | 7 +++- .../PendingRequests/PendingRequests.tsx | 25 ++++++++++-- .../views/UsersPage/PendingRequests/styles.ts | 26 ++++++++++++- 5 files changed, 84 insertions(+), 17 deletions(-) diff --git a/frontend/src/utils/api/users/admins.ts b/frontend/src/utils/api/users/admins.ts index 437e1eb56..c22282509 100644 --- a/frontend/src/utils/api/users/admins.ts +++ b/frontend/src/utils/api/users/admins.ts @@ -42,3 +42,7 @@ export async function addAdmin(userId: number) { export async function removeAdmin(userId: number) { alert("remove " + userId + " as admin"); } + +export async function removeAdminAndCoach(userId: number) { + alert("remove " + userId + " as admin & coach"); +} diff --git a/frontend/src/views/AdminsPage/Admins/Admins.tsx b/frontend/src/views/AdminsPage/Admins/Admins.tsx index 39df4af38..97911dd1d 100644 --- a/frontend/src/views/AdminsPage/Admins/Admins.tsx +++ b/frontend/src/views/AdminsPage/Admins/Admins.tsx @@ -1,8 +1,20 @@ import React, { useEffect, useState } from "react"; -import { AdminsContainer, AdminsTable, ModalContent, AddAdminButton, Warning } from "./styles"; +import { + AdminsContainer, + AdminsTable, + ModalContentGreen, + ModalContentRed, + AddAdminButton, + Warning, +} from "./styles"; import { getUsers, User } from "../../../utils/api/users/users"; import { Button, Modal, Spinner } from "react-bootstrap"; -import { addAdmin, getAdmins, removeAdmin } from "../../../utils/api/users/admins"; +import { + addAdmin, + getAdmins, + removeAdmin, + removeAdminAndCoach, +} from "../../../utils/api/users/admins"; import { SearchInput, SpinnerContainer } from "../../UsersPage/PendingRequests/styles"; import { Typeahead } from "react-bootstrap-typeahead"; @@ -16,7 +28,11 @@ function AdminFilter(props: { function AddWarning(props: { name: string | undefined }) { if (props.name !== undefined) { - return Warning: {props.name} will be able to edit/delete all data. ; + return ( + + Warning: {props.name} will be able to edit/delete all data and manage admin roles. + + ); } return null; } @@ -38,19 +54,20 @@ function AddAdmin(props: { users: User[] }) { - + Add Admin { - // @ts-ignore - setSelected(selected[0]); + setSelected(selected[0] as User); }} options={props.users} - labelKey={user => `${user.name} (${user.email})`} + labelKey="email" filterBy={["email", "name"]} + emptyLabel="No users found." + placeholder={"email"} /> @@ -71,7 +88,7 @@ function AddAdmin(props: { users: User[] }) { Cancel - + ); @@ -90,7 +107,7 @@ function RemoveAdmin(props: { admin: User }) { - + Remove Admin @@ -114,7 +131,7 @@ function RemoveAdmin(props: { admin: User }) { - + ); diff --git a/frontend/src/views/AdminsPage/Admins/styles.ts b/frontend/src/views/AdminsPage/Admins/styles.ts index 75de16bb6..3f0534424 100644 --- a/frontend/src/views/AdminsPage/Admins/styles.ts +++ b/frontend/src/views/AdminsPage/Admins/styles.ts @@ -9,11 +9,16 @@ export const AdminsContainer = styled.div` export const AdminsTable = styled(Table)``; -export const ModalContent = styled.div` +export const ModalContentGreen = styled.div` border: 3px solid var(--osoc_green); background-color: var(--osoc_blue); `; +export const ModalContentRed = styled.div` + border: 3px solid var(--osoc_red); + background-color: var(--osoc_blue); +`; + export const AddAdminButton = styled(Button).attrs({ size: "sm", })` diff --git a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx index 3b02b0766..44eb40d64 100644 --- a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx +++ b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx @@ -2,12 +2,16 @@ import React, { useEffect, useState } from "react"; import Collapsible from "react-collapsible"; import { RequestHeaderTitle, + RequestHeaderDiv, + OpenArrow, + ClosedArrow, RequestsTable, PendingRequestsContainer, AcceptButton, RejectButton, SpinnerContainer, SearchInput, + AcceptRejectTh, } from "./styles"; import { acceptRequest, @@ -17,8 +21,21 @@ import { } from "../../../utils/api/users/requests"; import { Spinner } from "react-bootstrap"; +function Arrow(props: { open: boolean }) { + if (props.open) { + return ; + } else { + return ; + } +} + function RequestHeader(props: { open: boolean }) { - return Requests; + return ( + + Requests + + + ); } function RequestFilter(props: { searchTerm: string; filter: (key: string) => void }) { @@ -71,7 +88,7 @@ function RequestsList(props: { requests: Request[]; loading: boolean }) { Name Email - Accept/Reject + Accept/Reject {body} @@ -121,8 +138,8 @@ export default function PendingRequests(props: { edition: string }) { } - onOpen={() => setOpen(true)} - onClose={() => setOpen(false)} + onOpening={() => setOpen(true)} + onClosing={() => setOpen(false)} > filter(word)} /> diff --git a/frontend/src/views/UsersPage/PendingRequests/styles.ts b/frontend/src/views/UsersPage/PendingRequests/styles.ts index 6c004c5ff..5cf5e95b0 100644 --- a/frontend/src/views/UsersPage/PendingRequests/styles.ts +++ b/frontend/src/views/UsersPage/PendingRequests/styles.ts @@ -1,18 +1,36 @@ import styled from "styled-components"; import { Table, Button } from "react-bootstrap"; +import { BiDownArrow } from "react-icons/bi"; + +export const RequestHeaderDiv = styled.div` + display: inline-block; +`; export const RequestHeaderTitle = styled.div` padding-bottom: 3px; padding-left: 3px; width: 100px; font-size: 25px; + float: left; +`; + +export const OpenArrow = styled(BiDownArrow)` + margin-top: 13px; + margin-left: 10px; + offset-position: 0px 30px; +`; + +export const ClosedArrow = styled(BiDownArrow)` + margin-top: 13px; + margin-left: 10px; + transform: rotate(-90deg); + offset: 0px 30px; `; export const SearchInput = styled.input.attrs({ placeholder: "Search", })` margin: 3px; - height: 20px; width: 150px; font-size: 15px; border-radius: 5px; @@ -27,12 +45,17 @@ export const PendingRequestsContainer = styled.div` margin: 10px auto auto; `; +export const AcceptRejectTh = styled.th` + width: 150px; +`; + 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)` @@ -42,6 +65,7 @@ export const RejectButton = styled(Button)` padding-bottom: 3px; padding-left: 3px; padding-right: 3px; + width: 65px; `; export const SpinnerContainer = styled.div` From 9625d9961b07bf6ff85f1018ca1b66c442a9f613 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sat, 2 Apr 2022 20:42:07 +0200 Subject: [PATCH 203/536] Add coach to edition --- frontend/src/utils/api/users/coaches.ts | 4 + .../src/views/AdminsPage/Admins/Admins.tsx | 2 +- .../src/views/UsersPage/Coaches/Coaches.tsx | 78 ++++++++++++++++++- .../src/views/UsersPage/Coaches/styles.ts | 17 ---- 4 files changed, 81 insertions(+), 20 deletions(-) diff --git a/frontend/src/utils/api/users/coaches.ts b/frontend/src/utils/api/users/coaches.ts index 43fc40bda..b55116cac 100644 --- a/frontend/src/utils/api/users/coaches.ts +++ b/frontend/src/utils/api/users/coaches.ts @@ -36,3 +36,7 @@ export async function removeCoachFromEdition(userId: number, edition: string) { export async function removeCoachFromAllEditions(userId: number) { alert("remove " + userId + " from all editions"); } + +export async function addCoachToEdition(userId: number, edition: string) { + alert("add " + userId + " to " + edition); +} diff --git a/frontend/src/views/AdminsPage/Admins/Admins.tsx b/frontend/src/views/AdminsPage/Admins/Admins.tsx index 97911dd1d..e0651636f 100644 --- a/frontend/src/views/AdminsPage/Admins/Admins.tsx +++ b/frontend/src/views/AdminsPage/Admins/Admins.tsx @@ -102,7 +102,7 @@ function RemoveAdmin(props: { admin: User }) { return ( <> - diff --git a/frontend/src/views/UsersPage/Coaches/Coaches.tsx b/frontend/src/views/UsersPage/Coaches/Coaches.tsx index 38cc5f9fb..4c6c72ea5 100644 --- a/frontend/src/views/UsersPage/Coaches/Coaches.tsx +++ b/frontend/src/views/UsersPage/Coaches/Coaches.tsx @@ -1,13 +1,16 @@ import React, { useEffect, useState } from "react"; import { CoachesTitle, CoachesContainer, CoachesTable, ModalContent } from "./styles"; -import { User } from "../../../utils/api/users/users"; +import { getUsers, User } from "../../../utils/api/users/users"; import { SearchInput, SpinnerContainer } from "../PendingRequests/styles"; import { Button, Modal, Spinner } from "react-bootstrap"; import { getCoaches, removeCoachFromAllEditions, removeCoachFromEdition, + addCoachToEdition, } from "../../../utils/api/users/coaches"; +import { AddAdminButton, ModalContentGreen } from "../../AdminsPage/Admins/styles"; +import { Typeahead } from "react-bootstrap-typeahead"; function CoachesHeader() { return Coaches; @@ -21,6 +24,62 @@ function CoachFilter(props: { return props.filter(e.target.value)} />; } +function AddCoach(props: { users: User[]; edition: string }) { + const [show, setShow] = useState(false); + const [selected, setSelected] = useState(undefined); + + const handleClose = () => { + setSelected(undefined); + setShow(false); + }; + const handleShow = () => setShow(true); + + return ( + <> + + Add coach + + + + + + Add Coach + + + { + setSelected(selected[0] as User); + }} + options={props.users} + labelKey="email" + filterBy={["email", "name"]} + emptyLabel="No users found." + placeholder={"user's email address"} + /> + + + + + + + + + ); +} + function RemoveCoach(props: { coach: User; edition: string }) { const [show, setShow] = useState(false); @@ -29,7 +88,7 @@ function RemoveCoach(props: { coach: User; edition: string }) { return ( <> - @@ -119,6 +178,7 @@ function CoachesList(props: { coaches: User[]; loading: boolean; edition: string export default function Coaches(props: { edition: string }) { const [allCoaches, setAllCoaches] = useState([]); const [coaches, setCoaches] = useState([]); + const [users, setUsers] = useState([]); const [gettingCoaches, setGettingCoaches] = useState(true); const [searchTerm, setSearchTerm] = useState(""); const [gotData, setGotData] = useState(false); @@ -136,6 +196,19 @@ export default function Coaches(props: { edition: string }) { console.log(error); setGettingCoaches(false); }); + getUsers() + .then(response => { + const users = []; + for (const user of response.users) { + if (!allCoaches.some(e => e.id === user.id)) { + users.push(user); + } + } + setUsers(users); + }) + .catch(function (error: any) { + console.log(error); + }); } }); @@ -161,6 +234,7 @@ export default function Coaches(props: { edition: string }) { searchTerm={searchTerm} filter={word => filter(word)} /> + ); diff --git a/frontend/src/views/UsersPage/Coaches/styles.ts b/frontend/src/views/UsersPage/Coaches/styles.ts index 1f8970bd5..1d388b8f1 100644 --- a/frontend/src/views/UsersPage/Coaches/styles.ts +++ b/frontend/src/views/UsersPage/Coaches/styles.ts @@ -14,25 +14,8 @@ export const CoachesTitle = styled.div` font-size: 25px; `; -export const RemoveFromEditionButton = styled.button` - background-color: var(--osoc_red); - margin-left: 3px; - padding-bottom: 3px; - padding-left: 3px; - padding-right: 3px; -`; - export const CoachesTable = styled(Table)``; -export const PopupDiv = styled.div` - background-color: var(--osoc_red); - width: 200px; - height: 100px; - position: absolute; - right: 0; - top: 0; -`; - export const ModalContent = styled.div` border: 3px solid var(--osoc_red); background-color: var(--osoc_blue); From d1fc4f7b74c339c7265293cc2d355549623ace23 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 3 Apr 2022 12:10:58 +0200 Subject: [PATCH 204/536] Get requests api --- frontend/src/utils/api/users/requests.ts | 42 +------------ .../PendingRequests/PendingRequests.tsx | 63 +++++++++++++------ .../views/UsersPage/PendingRequests/styles.ts | 4 ++ 3 files changed, 51 insertions(+), 58 deletions(-) diff --git a/frontend/src/utils/api/users/requests.ts b/frontend/src/utils/api/users/requests.ts index a1716e187..ad2282cc8 100644 --- a/frontend/src/utils/api/users/requests.ts +++ b/frontend/src/utils/api/users/requests.ts @@ -1,4 +1,5 @@ import { User } from "./users"; +import { axiosInstance } from "../api"; export interface Request { id: number; @@ -10,45 +11,8 @@ export interface GetRequestsResponse { } export async function getRequests(edition: string): Promise { - const data = { - requests: [ - { - id: 1, - user: { - id: 1, - name: "Seppe", - email: "seppe@mail.be", - admin: false, - }, - }, - { - id: 2, - user: { - id: 2, - name: "Stijn", - email: "stijn@mail.be", - admin: false, - }, - }, - ], - }; - - // eslint-disable-next-line promise/param-names - const delay = () => new Promise(res => setTimeout(res, 1000)); - await delay(); - - return data; - - // try { - // await axiosInstance - // .get(`/users/requests/?edition=${edition}`) - // .then(response => { - // return response.data; - // } - // ) - // } catch (error) { - // - // } + const response = await axiosInstance.get(`/users/requests/?edition=${edition}`); + return response.data as GetRequestsResponse; } export async function acceptRequest(requestId: number) { diff --git a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx index 44eb40d64..dd887749f 100644 --- a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx +++ b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx @@ -12,6 +12,7 @@ import { SpinnerContainer, SearchInput, AcceptRejectTh, + Error, } from "./styles"; import { acceptRequest, @@ -38,8 +39,18 @@ function RequestHeader(props: { open: boolean }) { ); } -function RequestFilter(props: { searchTerm: string; filter: (key: string) => void }) { - return props.filter(e.target.value)} />; +function RequestFilter(props: { + searchTerm: string; + filter: (key: string) => void; + show: boolean; +}) { + if (props.show) { + return ( + props.filter(e.target.value)} /> + ); + } else { + return null; + } } function AcceptReject(props: { requestId: number }) { @@ -63,7 +74,7 @@ function RequestItem(props: { request: Request }) { ); } -function RequestsList(props: { requests: Request[]; loading: boolean }) { +function RequestsList(props: { requests: Request[]; loading: boolean; gotData: boolean }) { if (props.loading) { return ( @@ -71,7 +82,11 @@ function RequestsList(props: { requests: Request[]; loading: boolean }) { ); } else if (props.requests.length === 0) { - return
No requests
; + if (props.gotData) { + return
No requests
; + } else { + return null; + } } const body = ( @@ -99,24 +114,29 @@ function RequestsList(props: { requests: Request[]; loading: boolean }) { export default function PendingRequests(props: { edition: string }) { const [allRequests, setAllRequests] = useState([]); const [requests, setRequests] = useState([]); - const [gettingRequests, setGettingRequests] = useState(true); + const [gettingRequests, setGettingRequests] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [gotData, setGotData] = useState(false); const [open, setOpen] = useState(false); + const [error, setError] = useState(""); + + async function getData() { + try { + const response = await getRequests(props.edition); + setAllRequests(response.requests); + setRequests(response.requests); + setGotData(true); + setGettingRequests(false); + } catch (exception) { + setError("Oops, something went wrong..."); + setGettingRequests(false); + } + } useEffect(() => { - if (!gotData) { - getRequests(props.edition) - .then(response => { - setRequests(response.requests); - setAllRequests(response.requests); - setGettingRequests(false); - setGotData(true); - }) - .catch(function (error: any) { - console.log(error); - setGettingRequests(false); - }); + if (!gotData && !gettingRequests && !error) { + setGettingRequests(true); + getData(); } }); @@ -141,8 +161,13 @@ export default function PendingRequests(props: { edition: string }) { onOpening={() => setOpen(true)} onClosing={() => setOpen(false)} > - filter(word)} /> - + filter(word)} + show={requests.length > 0} + /> + + {error}
); diff --git a/frontend/src/views/UsersPage/PendingRequests/styles.ts b/frontend/src/views/UsersPage/PendingRequests/styles.ts index 5cf5e95b0..03be79ef6 100644 --- a/frontend/src/views/UsersPage/PendingRequests/styles.ts +++ b/frontend/src/views/UsersPage/PendingRequests/styles.ts @@ -74,3 +74,7 @@ export const SpinnerContainer = styled.div` align-items: center; margin: 20px; `; + +export const Error = styled.div` + color: var(--osoc_red); +`; From 795464eba727d5e2c520119454eef60dc22c7948 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 3 Apr 2022 13:15:29 +0200 Subject: [PATCH 205/536] Get coaches api --- frontend/src/Router.tsx | 2 +- frontend/src/components/navbar/NavBar.tsx | 2 +- frontend/src/utils/api/users/coaches.ts | 25 +----- .../src/views/UsersPage/Coaches/Coaches.tsx | 77 +++++++++++-------- 4 files changed, 52 insertions(+), 54 deletions(-) diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 828409fa2..5b8eee551 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -37,7 +37,7 @@ export default function Router() { }> } /> - }> + }> } /> } /> diff --git a/frontend/src/components/navbar/NavBar.tsx b/frontend/src/components/navbar/NavBar.tsx index 76a3e54cb..09ff8867b 100644 --- a/frontend/src/components/navbar/NavBar.tsx +++ b/frontend/src/components/navbar/NavBar.tsx @@ -24,7 +24,7 @@ function NavBar() {
Students Projects - Users + Users { diff --git a/frontend/src/utils/api/users/coaches.ts b/frontend/src/utils/api/users/coaches.ts index b55116cac..e31d96140 100644 --- a/frontend/src/utils/api/users/coaches.ts +++ b/frontend/src/utils/api/users/coaches.ts @@ -1,32 +1,13 @@ import { User } from "./users"; +import { axiosInstance } from "../api"; export interface GetCoachesResponse { coaches: User[]; } export async function getCoaches(edition: string): Promise { - const data = { - coaches: [ - { - id: 3, - name: "Bert", - email: "bert@mail.be", - admin: false, - }, - { - id: 4, - name: "Tiebe", - email: "tiebe@mail.be", - admin: false, - }, - ], - }; - - // eslint-disable-next-line promise/param-names - const delay = () => new Promise(res => setTimeout(res, 100)); - await delay(); - - return data; + const response = await axiosInstance.get(`/users/?admin=false&edition=${edition}`); + return response.data as GetCoachesResponse; } export async function removeCoachFromEdition(userId: number, edition: string) { diff --git a/frontend/src/views/UsersPage/Coaches/Coaches.tsx b/frontend/src/views/UsersPage/Coaches/Coaches.tsx index 4c6c72ea5..49e5c79ab 100644 --- a/frontend/src/views/UsersPage/Coaches/Coaches.tsx +++ b/frontend/src/views/UsersPage/Coaches/Coaches.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { CoachesTitle, CoachesContainer, CoachesTable, ModalContent } from "./styles"; import { getUsers, User } from "../../../utils/api/users/users"; -import { SearchInput, SpinnerContainer } from "../PendingRequests/styles"; +import { Error, SearchInput, SpinnerContainer } from "../PendingRequests/styles"; import { Button, Modal, Spinner } from "react-bootstrap"; import { getCoaches, @@ -142,7 +142,12 @@ function CoachItem(props: { coach: User; edition: string }) { ); } -function CoachesList(props: { coaches: User[]; loading: boolean; edition: string }) { +function CoachesList(props: { + coaches: User[]; + loading: boolean; + edition: string; + gotData: boolean; +}) { if (props.loading) { return ( @@ -150,7 +155,11 @@ function CoachesList(props: { coaches: User[]; loading: boolean; edition: string ); } else if (props.coaches.length === 0) { - return
No coaches for this edition
; + if (props.gotData) { + return
No coaches for this edition
; + } else { + return null; + } } const body = ( @@ -179,36 +188,38 @@ export default function Coaches(props: { edition: string }) { const [allCoaches, setAllCoaches] = useState([]); const [coaches, setCoaches] = useState([]); const [users, setUsers] = useState([]); - const [gettingCoaches, setGettingCoaches] = useState(true); + const [gettingData, setGettingData] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [gotData, setGotData] = useState(false); + const [error, setError] = useState(""); + + async function getData() { + try { + const coachResponse = await getCoaches(props.edition); + setAllCoaches(coachResponse.coaches); + setCoaches(coachResponse.coaches); + + const UsersResponse = await getUsers(); + const users = []; + for (const user of UsersResponse.users) { + if (!allCoaches.some(e => e.id === user.id)) { + users.push(user); + } + } + setUsers(users); + + setGotData(true); + setGettingData(false); + } catch (exception) { + setError("Oops, something went wrong..."); + setGettingData(false); + } + } useEffect(() => { - if (!gotData) { - getCoaches(props.edition) - .then(response => { - setCoaches(response.coaches); - setAllCoaches(response.coaches); - setGettingCoaches(false); - setGotData(true); - }) - .catch(function (error: any) { - console.log(error); - setGettingCoaches(false); - }); - getUsers() - .then(response => { - const users = []; - for (const user of response.users) { - if (!allCoaches.some(e => e.id === user.id)) { - users.push(user); - } - } - setUsers(users); - }) - .catch(function (error: any) { - console.log(error); - }); + if (!gotData && !gettingData && !error) { + setGettingData(true); + getData(); } }); @@ -235,7 +246,13 @@ export default function Coaches(props: { edition: string }) { filter={word => filter(word)} /> - + + {error} ); } From 94769e330fe86fd1bc5fad1d244c64ca6ff7cc96 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 3 Apr 2022 13:26:40 +0200 Subject: [PATCH 206/536] placeholders for header --- frontend/src/views/ProjectsPage/ProjectsPage.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index e084fa9d5..69d663440 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -30,6 +30,12 @@ function ProjectPage() { return (
+
+ + + +
+ {projects.map((project, _index) => ( ))} - Date: Sun, 3 Apr 2022 13:30:17 +0200 Subject: [PATCH 207/536] Fix typo --- frontend/src/utils/api/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/utils/api/api.ts b/frontend/src/utils/api/api.ts index e88e6b3d2..de7a51812 100644 --- a/frontend/src/utils/api/api.ts +++ b/frontend/src/utils/api/api.ts @@ -15,5 +15,5 @@ export function setBearerToken(value: string | null) { return; } - axiosInstance.defaults.headers.common.Authorization = value; + axiosInstance.defaults.headers.common.Authorization = `Bearer ${value}`; } From aa221d67a0e74be0daed167738110b06eca2961b Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 3 Apr 2022 15:24:57 +0200 Subject: [PATCH 208/536] admin api --- frontend/src/Router.tsx | 4 + frontend/src/components/navbar/NavBar.tsx | 2 +- frontend/src/utils/api/users/admins.ts | 45 ++---- frontend/src/utils/api/users/coaches.ts | 4 +- frontend/src/utils/api/users/requests.ts | 4 +- frontend/src/utils/api/users/users.ts | 56 +------ .../src/views/AdminsPage/Admins/Admins.tsx | 149 ++++++++++++------ .../src/views/UsersPage/Coaches/Coaches.tsx | 26 ++- .../PendingRequests/PendingRequests.tsx | 13 +- 9 files changed, 140 insertions(+), 163 deletions(-) diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 5b8eee551..c7ffc31cf 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -15,6 +15,7 @@ import PrivateRoute from "./components/PrivateRoute"; import AdminRoute from "./components/AdminRoute"; import { NotFoundPage } from "./views/errors"; import ForbiddenPage from "./views/errors/ForbiddenPage"; +import AdminsPage from "./views/AdminsPage/AdminsPage"; export default function Router() { const { isLoggedIn } = useAuth(); @@ -40,6 +41,9 @@ export default function Router() { }> } /> + }> + } /> + } /> } /> } /> diff --git a/frontend/src/components/navbar/NavBar.tsx b/frontend/src/components/navbar/NavBar.tsx index 09ff8867b..cd6ff3cec 100644 --- a/frontend/src/components/navbar/NavBar.tsx +++ b/frontend/src/components/navbar/NavBar.tsx @@ -24,7 +24,7 @@ function NavBar() {
Students Projects - Users + Users { diff --git a/frontend/src/utils/api/users/admins.ts b/frontend/src/utils/api/users/admins.ts index c22282509..1d8167153 100644 --- a/frontend/src/utils/api/users/admins.ts +++ b/frontend/src/utils/api/users/admins.ts @@ -1,48 +1,27 @@ import { User } from "./users"; +import { axiosInstance } from "../api"; export interface GetAdminsResponse { - admins: User[]; + users: User[]; } export async function getAdmins(): Promise { - const data = { - admins: [ - { - id: 5, - name: "Ward", - email: "ward@mail.be", - admin: true, - }, - { - id: 6, - name: "Francis", - email: "francis@mail.be", - admin: true, - }, - { - id: 7, - name: "Clement", - email: "clement@mail.be", - admin: true, - }, - ], - }; - - // eslint-disable-next-line promise/param-names - const delay = () => new Promise(res => setTimeout(res, 100)); - await delay(); - - return data; + const response = await axiosInstance.get(`/users?admin=true`); + return response.data as GetAdminsResponse; } -export async function addAdmin(userId: number) { - alert("add " + userId + " as admin"); +export async function addAdmin(userId: number): Promise { + const response = await axiosInstance.patch(`/users/${userId}`, { admin: true }); + return response.status === 204; } export async function removeAdmin(userId: number) { - alert("remove " + userId + " as admin"); + const response = await axiosInstance.patch(`/users/${userId}`, { admin: false }); + return response.status === 204; } export async function removeAdminAndCoach(userId: number) { - alert("remove " + userId + " as admin & coach"); + const response = await axiosInstance.patch(`/users/${userId}`, { admin: false }); + // TODO: remove user from all editions + return response.status === 204; } diff --git a/frontend/src/utils/api/users/coaches.ts b/frontend/src/utils/api/users/coaches.ts index e31d96140..f6dfc9246 100644 --- a/frontend/src/utils/api/users/coaches.ts +++ b/frontend/src/utils/api/users/coaches.ts @@ -2,11 +2,11 @@ import { User } from "./users"; import { axiosInstance } from "../api"; export interface GetCoachesResponse { - coaches: User[]; + users: User[]; } export async function getCoaches(edition: string): Promise { - const response = await axiosInstance.get(`/users/?admin=false&edition=${edition}`); + const response = await axiosInstance.get(`/users/?edition=${edition}`); return response.data as GetCoachesResponse; } diff --git a/frontend/src/utils/api/users/requests.ts b/frontend/src/utils/api/users/requests.ts index ad2282cc8..103356139 100644 --- a/frontend/src/utils/api/users/requests.ts +++ b/frontend/src/utils/api/users/requests.ts @@ -2,7 +2,7 @@ import { User } from "./users"; import { axiosInstance } from "../api"; export interface Request { - id: number; + requestId: number; user: User; } @@ -11,7 +11,7 @@ export interface GetRequestsResponse { } export async function getRequests(edition: string): Promise { - const response = await axiosInstance.get(`/users/requests/?edition=${edition}`); + const response = await axiosInstance.get(`/users/requests?edition=${edition}`); return response.data as GetRequestsResponse; } diff --git a/frontend/src/utils/api/users/users.ts b/frontend/src/utils/api/users/users.ts index 5a3bdfb14..54dcb7fe7 100644 --- a/frontend/src/utils/api/users/users.ts +++ b/frontend/src/utils/api/users/users.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { axiosInstance } from "../api"; export interface User { - id: number; + userId: number; name: string; email: string; admin: boolean; @@ -33,56 +33,6 @@ export interface GetUsersResponse { } export async function getUsers(): Promise { - const data = { - users: [ - { - id: 1, - name: "Seppe", - email: "seppe@mail.be", - admin: false, - }, - { - id: 2, - name: "Stijn", - email: "stijn@mail.be", - admin: false, - }, - { - id: 3, - name: "Bert", - email: "bert@mail.be", - admin: false, - }, - { - id: 4, - name: "Tiebe", - email: "tiebe@mail.be", - admin: false, - }, - { - id: 5, - name: "Ward", - email: "ward@mail.be", - admin: true, - }, - { - id: 6, - name: "Francis", - email: "francis@mail.be", - admin: true, - }, - { - id: 7, - name: "Clement", - email: "clement@mail.be", - admin: true, - }, - ], - }; - - // eslint-disable-next-line promise/param-names - const delay = () => new Promise(res => setTimeout(res, 100)); - await delay(); - - return data; + const response = await axiosInstance.get(`/users`); + return response.data as GetUsersResponse; } diff --git a/frontend/src/views/AdminsPage/Admins/Admins.tsx b/frontend/src/views/AdminsPage/Admins/Admins.tsx index e0651636f..8804f5182 100644 --- a/frontend/src/views/AdminsPage/Admins/Admins.tsx +++ b/frontend/src/views/AdminsPage/Admins/Admins.tsx @@ -15,7 +15,7 @@ import { removeAdmin, removeAdminAndCoach, } from "../../../utils/api/users/admins"; -import { SearchInput, SpinnerContainer } from "../../UsersPage/PendingRequests/styles"; +import { Error, SearchInput, SpinnerContainer } from "../../UsersPage/PendingRequests/styles"; import { Typeahead } from "react-bootstrap-typeahead"; function AdminFilter(props: { @@ -37,15 +37,33 @@ function AddWarning(props: { name: string | undefined }) { return null; } -function AddAdmin(props: { users: User[] }) { +function AddAdmin(props: { users: User[]; refresh: () => void }) { const [show, setShow] = useState(false); const [selected, setSelected] = useState(undefined); + const [error, setError] = useState(""); const handleClose = () => { setSelected(undefined); setShow(false); }; - const handleShow = () => setShow(true); + const handleShow = () => { + setShow(true); + setError(""); + }; + + async function addUserAsAdmin(userId: number) { + try { + const added = await addAdmin(userId); + if (added) { + props.refresh(); + handleClose(); + } else { + setError("Something went wrong. Failed to add admin"); + } + } catch (error) { + setError("Something went wrong. Failed to add admin"); + } + } return ( <> @@ -63,11 +81,11 @@ function AddAdmin(props: { users: User[] }) { onChange={selected => { setSelected(selected[0] as User); }} + id="non-admin-users" options={props.users} - labelKey="email" - filterBy={["email", "name"]} + labelKey="name" emptyLabel="No users found." - placeholder={"email"} + placeholder={"name"} /> @@ -76,9 +94,8 @@ function AddAdmin(props: { users: User[] }) { variant="primary" onClick={() => { if (selected !== undefined) { - addAdmin(selected.id); + addUserAsAdmin(selected.userId); } - handleClose(); }} disabled={selected === undefined} > @@ -87,6 +104,7 @@ function AddAdmin(props: { users: User[] }) { + {error} @@ -94,12 +112,33 @@ function AddAdmin(props: { users: User[] }) { ); } -function RemoveAdmin(props: { admin: User }) { +function RemoveAdmin(props: { admin: User; refresh: () => void }) { const [show, setShow] = useState(false); + const [error, setError] = useState(""); const handleClose = () => setShow(false); const handleShow = () => setShow(true); + 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(); + handleClose(); + } else { + setError("Something went wrong. Failed to remove admin"); + } + } catch (error) { + setError("Something went wrong. Failed to remove admin"); + } + } + return ( <> + {error} @@ -147,19 +187,24 @@ function RemoveAdmin(props: { admin: User }) { ); } -function AdminItem(props: { admin: User }) { +function AdminItem(props: { admin: User; refresh: () => void }) { return ( {props.admin.name} {props.admin.email} - + ); } -function AdminsList(props: { admins: User[]; loading: boolean }) { +function AdminsList(props: { + admins: User[]; + loading: boolean; + gotData: boolean; + refresh: () => void; +}) { if (props.loading) { return ( @@ -167,13 +212,17 @@ function AdminsList(props: { admins: User[]; loading: boolean }) { ); } else if (props.admins.length === 0) { - return
No admins? #rip
; + if (props.gotData) { + return
No admins
; + } else { + return null; + } } const body = ( {props.admins.map(admin => ( - + ))} ); @@ -196,47 +245,46 @@ export default function Admins() { const [allAdmins, setAllAdmins] = useState([]); const [admins, setAdmins] = useState([]); const [users, setUsers] = useState([]); - const [gettingAdmins, setGettingAdmins] = useState(true); + const [gettingData, setGettingData] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [gotData, setGotData] = useState(false); + const [error, setError] = useState(""); + + async function getData() { + setGettingData(true); + try { + const response = await getAdmins(); + setAllAdmins(response.users); + setAdmins(response.users); + + const usersResponse = await getUsers(); + const users = []; + for (const user of usersResponse.users) { + if (!response.users.some(e => e.userId === user.userId)) { + users.push(user); + } + } + setUsers(users); + + setGotData(true); + setGettingData(false); + } catch (exception) { + setError("Oops, something went wrong..."); + setGettingData(false); + } + } useEffect(() => { - if (!gotData) { - getAdmins() - .then(response => { - setAdmins(response.admins); - setAllAdmins(response.admins); - setGettingAdmins(false); - setGotData(true); - }) - .catch(function (error: any) { - console.log(error); - setGettingAdmins(false); - }); - getUsers() - .then(response => { - const users = []; - for (const user of response.users) { - if (!allAdmins.some(e => e.id === user.id)) { - users.push(user); - } - } - setUsers(users); - }) - .catch(function (error: any) { - console.log(error); - }); + if (!gotData && !gettingData && !error) { + getData(); } - }); + }, [gotData, gettingData, error, getData]); const filter = (word: string) => { setSearchTerm(word); const newCoaches: User[] = []; for (const admin of allAdmins) { - if ( - admin.name.toUpperCase().includes(word.toUpperCase()) || - admin.email.toUpperCase().includes(word.toUpperCase()) - ) { + if (admin.name.toUpperCase().includes(word.toUpperCase())) { newCoaches.push(admin); } } @@ -246,12 +294,13 @@ export default function Admins() { return ( 0} + search={allAdmins.length > 0} searchTerm={searchTerm} filter={word => filter(word)} /> - - + + + {error} ); } diff --git a/frontend/src/views/UsersPage/Coaches/Coaches.tsx b/frontend/src/views/UsersPage/Coaches/Coaches.tsx index 49e5c79ab..76b6acd20 100644 --- a/frontend/src/views/UsersPage/Coaches/Coaches.tsx +++ b/frontend/src/views/UsersPage/Coaches/Coaches.tsx @@ -50,11 +50,12 @@ function AddCoach(props: { users: User[]; edition: string }) { onChange={selected => { setSelected(selected[0] as User); }} + id="non-coach-users" options={props.users} - labelKey="email" + labelKey="name" filterBy={["email", "name"]} emptyLabel="No users found." - placeholder={"user's email address"} + placeholder={"user's name"} /> @@ -62,7 +63,7 @@ function AddCoach(props: { users: User[]; edition: string }) { variant="primary" onClick={() => { if (selected !== undefined) { - addCoachToEdition(selected.id, props.edition); + addCoachToEdition(selected.userId, props.edition); } handleClose(); }} @@ -105,7 +106,7 @@ function RemoveCoach(props: { coach: User; edition: string }) { + {error} @@ -81,11 +96,35 @@ function AddCoach(props: { users: User[]; edition: string }) { ); } -function RemoveCoach(props: { coach: User; edition: string }) { +function RemoveCoach(props: { coach: User; edition: string; refresh: () => void }) { const [show, setShow] = useState(false); + const [error, setError] = useState(""); const handleClose = () => setShow(false); - const handleShow = () => setShow(true); + const handleShow = () => { + setShow(true); + setError(""); + }; + + async function removeCoach(userId: number, allEditions: boolean) { + try { + let removed; + if (allEditions) { + removed = await removeCoachFromAllEditions(userId); + } else { + removed = await removeCoachFromEdition(userId, props.edition); + } + + if (removed) { + props.refresh(); + handleClose(); + } else { + setError("Something went wrong. Failed to remove coach"); + } + } catch (error) { + setError("Something went wrong. Failed to remove coach"); + } + } return ( <> @@ -106,8 +145,7 @@ function RemoveCoach(props: { coach: User; edition: string }) { + {error} @@ -131,13 +169,13 @@ function RemoveCoach(props: { coach: User; edition: string }) { ); } -function CoachItem(props: { coach: User; edition: string }) { +function CoachItem(props: { coach: User; edition: string; refresh: () => void }) { return ( {props.coach.name} {props.coach.email} - + ); @@ -148,6 +186,7 @@ function CoachesList(props: { loading: boolean; edition: string; gotData: boolean; + refresh: () => void; }) { if (props.loading) { return ( @@ -166,7 +205,12 @@ function CoachesList(props: { const body = ( {props.coaches.map(coach => ( - + ))} ); @@ -195,6 +239,8 @@ export default function Coaches(props: { edition: string }) { const [error, setError] = useState(""); async function getData() { + setGettingData(true); + setGotData(false); try { const coachResponse = await getCoaches(props.edition); setAllCoaches(coachResponse.users); @@ -203,7 +249,7 @@ export default function Coaches(props: { edition: string }) { const UsersResponse = await getUsers(); const users = []; for (const user of UsersResponse.users) { - if (!allCoaches.some(e => e.userId === user.userId)) { + if (!coachResponse.users.some(e => e.userId === user.userId)) { users.push(user); } } @@ -219,7 +265,6 @@ export default function Coaches(props: { edition: string }) { useEffect(() => { if (!gotData && !gettingData && !error) { - setGettingData(true); getData(); } }, [gotData, gettingData, error, getData]); @@ -243,12 +288,13 @@ export default function Coaches(props: { edition: string }) { searchTerm={searchTerm} filter={word => filter(word)} /> - + {error} From 40d2fdac227c180e0fbce508cce4214f2721abba Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 3 Apr 2022 16:41:26 +0200 Subject: [PATCH 211/536] Remove debug statement --- frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx index 3dbce8811..6fae95c40 100644 --- a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx +++ b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx @@ -28,8 +28,6 @@ export default function VerifyingTokenPage() { verifyToken(); }, [authContext]); - setBearerToken("test"); - // This will be replaced later on return

Loading...

; } From 75f9650b019f18631c75bda9ce9f591c1e9b3db9 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 3 Apr 2022 00:25:22 +0200 Subject: [PATCH 212/536] Add typedoc & create initial index files --- frontend/docs/.nojekyll | 1 + frontend/docs/assets/highlight.css | 85 + frontend/docs/assets/icons.css | 1043 ++++++++++++ frontend/docs/assets/icons.png | Bin 0 -> 9615 bytes frontend/docs/assets/icons@2x.png | Bin 0 -> 28144 bytes frontend/docs/assets/main.js | 52 + frontend/docs/assets/search.js | 1 + frontend/docs/assets/style.css | 1413 +++++++++++++++++ frontend/docs/assets/widgets.png | Bin 0 -> 480 bytes frontend/docs/assets/widgets@2x.png | Bin 0 -> 855 bytes frontend/docs/enums/Data.Enums.Role.html | 4 + .../docs/enums/Data.Enums.StorageKey.html | 3 + frontend/docs/index.html | 107 ++ .../interfaces/Contexts.AuthContextState.html | 1 + .../docs/interfaces/Data.Interfaces.User.html | 1 + frontend/docs/modules.html | 1 + frontend/docs/modules/Components.Login.html | 1 + .../docs/modules/Components.Register.html | 1 + frontend/docs/modules/Components.html | 7 + frontend/docs/modules/Contexts.html | 6 + frontend/docs/modules/Data.Enums.html | 1 + frontend/docs/modules/Data.Interfaces.html | 1 + frontend/docs/modules/Data.html | 1 + frontend/package.json | 4 +- frontend/src/components/index.ts | 7 + frontend/src/data/index.ts | 2 + frontend/src/index.ts | 3 + frontend/tsconfig.json | 52 +- frontend/yarn.lock | 61 +- 29 files changed, 2834 insertions(+), 25 deletions(-) create mode 100644 frontend/docs/.nojekyll create mode 100644 frontend/docs/assets/highlight.css create mode 100644 frontend/docs/assets/icons.css create mode 100644 frontend/docs/assets/icons.png create mode 100644 frontend/docs/assets/icons@2x.png create mode 100644 frontend/docs/assets/main.js create mode 100644 frontend/docs/assets/search.js create mode 100644 frontend/docs/assets/style.css create mode 100644 frontend/docs/assets/widgets.png create mode 100644 frontend/docs/assets/widgets@2x.png create mode 100644 frontend/docs/enums/Data.Enums.Role.html create mode 100644 frontend/docs/enums/Data.Enums.StorageKey.html create mode 100644 frontend/docs/index.html create mode 100644 frontend/docs/interfaces/Contexts.AuthContextState.html create mode 100644 frontend/docs/interfaces/Data.Interfaces.User.html create mode 100644 frontend/docs/modules.html create mode 100644 frontend/docs/modules/Components.Login.html create mode 100644 frontend/docs/modules/Components.Register.html create mode 100644 frontend/docs/modules/Components.html create mode 100644 frontend/docs/modules/Contexts.html create mode 100644 frontend/docs/modules/Data.Enums.html create mode 100644 frontend/docs/modules/Data.Interfaces.html create mode 100644 frontend/docs/modules/Data.html create mode 100644 frontend/src/components/index.ts create mode 100644 frontend/src/data/index.ts create mode 100644 frontend/src/index.ts diff --git a/frontend/docs/.nojekyll b/frontend/docs/.nojekyll new file mode 100644 index 000000000..e2ac6616a --- /dev/null +++ b/frontend/docs/.nojekyll @@ -0,0 +1 @@ +TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. \ No newline at end of file diff --git a/frontend/docs/assets/highlight.css b/frontend/docs/assets/highlight.css new file mode 100644 index 000000000..aea30d60d --- /dev/null +++ b/frontend/docs/assets/highlight.css @@ -0,0 +1,85 @@ +:root { + --light-hl-0: #000000; + --dark-hl-0: #D4D4D4; + --light-hl-1: #008000; + --dark-hl-1: #6A9955; + --light-hl-2: #AF00DB; + --dark-hl-2: #C586C0; + --light-hl-3: #0000FF; + --dark-hl-3: #569CD6; + --light-hl-4: #0070C1; + --dark-hl-4: #4FC1FF; + --light-hl-5: #098658; + --dark-hl-5: #B5CEA8; + --light-hl-6: #795E26; + --dark-hl-6: #DCDCAA; + --light-hl-7: #001080; + --dark-hl-7: #9CDCFE; + --light-hl-8: #A31515; + --dark-hl-8: #CE9178; + --light-code-background: #F5F5F5; + --dark-code-background: #1E1E1E; +} + +@media (prefers-color-scheme: light) { :root { + --hl-0: var(--light-hl-0); + --hl-1: var(--light-hl-1); + --hl-2: var(--light-hl-2); + --hl-3: var(--light-hl-3); + --hl-4: var(--light-hl-4); + --hl-5: var(--light-hl-5); + --hl-6: var(--light-hl-6); + --hl-7: var(--light-hl-7); + --hl-8: var(--light-hl-8); + --code-background: var(--light-code-background); +} } + +@media (prefers-color-scheme: dark) { :root { + --hl-0: var(--dark-hl-0); + --hl-1: var(--dark-hl-1); + --hl-2: var(--dark-hl-2); + --hl-3: var(--dark-hl-3); + --hl-4: var(--dark-hl-4); + --hl-5: var(--dark-hl-5); + --hl-6: var(--dark-hl-6); + --hl-7: var(--dark-hl-7); + --hl-8: var(--dark-hl-8); + --code-background: var(--dark-code-background); +} } + +body.light { + --hl-0: var(--light-hl-0); + --hl-1: var(--light-hl-1); + --hl-2: var(--light-hl-2); + --hl-3: var(--light-hl-3); + --hl-4: var(--light-hl-4); + --hl-5: var(--light-hl-5); + --hl-6: var(--light-hl-6); + --hl-7: var(--light-hl-7); + --hl-8: var(--light-hl-8); + --code-background: var(--light-code-background); +} + +body.dark { + --hl-0: var(--dark-hl-0); + --hl-1: var(--dark-hl-1); + --hl-2: var(--dark-hl-2); + --hl-3: var(--dark-hl-3); + --hl-4: var(--dark-hl-4); + --hl-5: var(--dark-hl-5); + --hl-6: var(--dark-hl-6); + --hl-7: var(--dark-hl-7); + --hl-8: var(--dark-hl-8); + --code-background: var(--dark-code-background); +} + +.hl-0 { color: var(--hl-0); } +.hl-1 { color: var(--hl-1); } +.hl-2 { color: var(--hl-2); } +.hl-3 { color: var(--hl-3); } +.hl-4 { color: var(--hl-4); } +.hl-5 { color: var(--hl-5); } +.hl-6 { color: var(--hl-6); } +.hl-7 { color: var(--hl-7); } +.hl-8 { color: var(--hl-8); } +pre, code { background: var(--code-background); } diff --git a/frontend/docs/assets/icons.css b/frontend/docs/assets/icons.css new file mode 100644 index 000000000..776a3562d --- /dev/null +++ b/frontend/docs/assets/icons.css @@ -0,0 +1,1043 @@ +.tsd-kind-icon { + display: block; + position: relative; + padding-left: 20px; + text-indent: -20px; +} +.tsd-kind-icon:before { + content: ""; + display: inline-block; + vertical-align: middle; + width: 17px; + height: 17px; + margin: 0 3px 2px 0; + background-image: url(./icons.png); +} +@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { + .tsd-kind-icon:before { + background-image: url(./icons@2x.png); + background-size: 238px 204px; + } +} + +.tsd-signature.tsd-kind-icon:before { + background-position: 0 -153px; +} + +.tsd-kind-object-literal > .tsd-kind-icon:before { + background-position: 0px -17px; +} +.tsd-kind-object-literal.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -17px; +} +.tsd-kind-object-literal.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -17px; +} + +.tsd-kind-class > .tsd-kind-icon:before { + background-position: 0px -34px; +} +.tsd-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -34px; +} +.tsd-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -34px; +} + +.tsd-kind-class.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: 0px -51px; +} +.tsd-kind-class.tsd-has-type-parameter.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -17px -51px; +} +.tsd-kind-class.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -51px; +} + +.tsd-kind-interface > .tsd-kind-icon:before { + background-position: 0px -68px; +} +.tsd-kind-interface.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -68px; +} +.tsd-kind-interface.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -68px; +} + +.tsd-kind-interface.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: 0px -85px; +} +.tsd-kind-interface.tsd-has-type-parameter.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -17px -85px; +} +.tsd-kind-interface.tsd-has-type-parameter.tsd-is-private + > .tsd-kind-icon:before { + background-position: -34px -85px; +} + +.tsd-kind-namespace > .tsd-kind-icon:before { + background-position: 0px -102px; +} +.tsd-kind-namespace.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -102px; +} +.tsd-kind-namespace.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -102px; +} + +.tsd-kind-module > .tsd-kind-icon:before { + background-position: 0px -102px; +} +.tsd-kind-module.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -102px; +} +.tsd-kind-module.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -102px; +} + +.tsd-kind-enum > .tsd-kind-icon:before { + background-position: 0px -119px; +} +.tsd-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -119px; +} +.tsd-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -119px; +} + +.tsd-kind-enum-member > .tsd-kind-icon:before { + background-position: 0px -136px; +} +.tsd-kind-enum-member.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -136px; +} +.tsd-kind-enum-member.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -136px; +} + +.tsd-kind-signature > .tsd-kind-icon:before { + background-position: 0px -153px; +} +.tsd-kind-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -153px; +} +.tsd-kind-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -153px; +} + +.tsd-kind-type-alias > .tsd-kind-icon:before { + background-position: 0px -170px; +} +.tsd-kind-type-alias.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -170px; +} +.tsd-kind-type-alias.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -170px; +} + +.tsd-kind-type-alias.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: 0px -187px; +} +.tsd-kind-type-alias.tsd-has-type-parameter.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -17px -187px; +} +.tsd-kind-type-alias.tsd-has-type-parameter.tsd-is-private + > .tsd-kind-icon:before { + background-position: -34px -187px; +} + +.tsd-kind-variable > .tsd-kind-icon:before { + background-position: -136px -0px; +} +.tsd-kind-variable.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -0px; +} +.tsd-kind-variable.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-variable.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -0px; +} +.tsd-kind-variable.tsd-parent-kind-enum.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -187px -0px; +} +.tsd-kind-variable.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-variable.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -0px; +} +.tsd-kind-variable.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -0px; +} + +.tsd-kind-property > .tsd-kind-icon:before { + background-position: -136px -0px; +} +.tsd-kind-property.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -0px; +} +.tsd-kind-property.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-property.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -0px; +} +.tsd-kind-property.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -0px; +} +.tsd-kind-property.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -0px; +} +.tsd-kind-property.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -0px; +} +.tsd-kind-property.tsd-parent-kind-class.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-property.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -0px; +} +.tsd-kind-property.tsd-parent-kind-enum.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -187px -0px; +} +.tsd-kind-property.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-property.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -0px; +} +.tsd-kind-property.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -0px; +} + +.tsd-kind-get-signature > .tsd-kind-icon:before { + background-position: -136px -17px; +} +.tsd-kind-get-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -17px; +} +.tsd-kind-get-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-enum.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -187px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-enum.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -17px; +} + +.tsd-kind-set-signature > .tsd-kind-icon:before { + background-position: -136px -34px; +} +.tsd-kind-set-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -34px; +} +.tsd-kind-set-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-enum.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -187px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-enum.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -34px; +} + +.tsd-kind-accessor > .tsd-kind-icon:before { + background-position: -136px -51px; +} +.tsd-kind-accessor.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -51px; +} +.tsd-kind-accessor.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-enum.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -187px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -51px; +} + +.tsd-kind-function > .tsd-kind-icon:before { + background-position: -136px -68px; +} +.tsd-kind-function.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -68px; +} +.tsd-kind-function.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-function.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -68px; +} +.tsd-kind-function.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -68px; +} +.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -68px; +} +.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -68px; +} +.tsd-kind-function.tsd-parent-kind-class.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-function.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -68px; +} +.tsd-kind-function.tsd-parent-kind-enum.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -187px -68px; +} +.tsd-kind-function.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-function.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -68px; +} +.tsd-kind-function.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -68px; +} + +.tsd-kind-method > .tsd-kind-icon:before { + background-position: -136px -68px; +} +.tsd-kind-method.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -68px; +} +.tsd-kind-method.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-method.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -68px; +} +.tsd-kind-method.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -68px; +} +.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -68px; +} +.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -68px; +} +.tsd-kind-method.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-method.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -68px; +} +.tsd-kind-method.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -68px; +} +.tsd-kind-method.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-method.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -68px; +} +.tsd-kind-method.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -68px; +} + +.tsd-kind-call-signature > .tsd-kind-icon:before { + background-position: -136px -68px; +} +.tsd-kind-call-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -68px; +} +.tsd-kind-call-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -187px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -68px; +} + +.tsd-kind-function.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: -136px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -153px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class + > .tsd-kind-icon:before { + background-position: -51px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum + > .tsd-kind-icon:before { + background-position: -170px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -187px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-interface + > .tsd-kind-icon:before { + background-position: -204px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -85px; +} + +.tsd-kind-method.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: -136px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -153px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class + > .tsd-kind-icon:before { + background-position: -51px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum + > .tsd-kind-icon:before { + background-position: -170px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -187px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-interface + > .tsd-kind-icon:before { + background-position: -204px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -85px; +} + +.tsd-kind-constructor > .tsd-kind-icon:before { + background-position: -136px -102px; +} +.tsd-kind-constructor.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -102px; +} +.tsd-kind-constructor.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-enum.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -187px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-enum.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -102px; +} + +.tsd-kind-constructor-signature > .tsd-kind-icon:before { + background-position: -136px -102px; +} +.tsd-kind-constructor-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -102px; +} +.tsd-kind-constructor-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-enum.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -187px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-enum.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-interface + > .tsd-kind-icon:before { + background-position: -204px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -102px; +} + +.tsd-kind-index-signature > .tsd-kind-icon:before { + background-position: -136px -119px; +} +.tsd-kind-index-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -119px; +} +.tsd-kind-index-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-enum.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -187px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-enum.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -119px; +} + +.tsd-kind-event > .tsd-kind-icon:before { + background-position: -136px -136px; +} +.tsd-kind-event.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -136px; +} +.tsd-kind-event.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -136px; +} +.tsd-kind-event.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -136px; +} +.tsd-kind-event.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -136px; +} +.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -136px; +} +.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -136px; +} +.tsd-kind-event.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -136px; +} +.tsd-kind-event.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -136px; +} +.tsd-kind-event.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -136px; +} +.tsd-kind-event.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -136px; +} +.tsd-kind-event.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -136px; +} +.tsd-kind-event.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -136px; +} + +.tsd-is-static > .tsd-kind-icon:before { + background-position: -136px -153px; +} +.tsd-is-static.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -153px; +} +.tsd-is-static.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -153px; +} +.tsd-is-static.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -153px; +} +.tsd-is-static.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -153px; +} +.tsd-is-static.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -153px; +} +.tsd-is-static.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -153px; +} +.tsd-is-static.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -153px; +} +.tsd-is-static.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -153px; +} +.tsd-is-static.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -153px; +} +.tsd-is-static.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -153px; +} +.tsd-is-static.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -153px; +} +.tsd-is-static.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -153px; +} + +.tsd-is-static.tsd-kind-function > .tsd-kind-icon:before { + background-position: -136px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -187px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-interface + > .tsd-kind-icon:before { + background-position: -204px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -170px; +} + +.tsd-is-static.tsd-kind-method > .tsd-kind-icon:before { + background-position: -136px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -187px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-interface + > .tsd-kind-icon:before { + background-position: -204px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -170px; +} + +.tsd-is-static.tsd-kind-call-signature > .tsd-kind-icon:before { + background-position: -136px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -153px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class + > .tsd-kind-icon:before { + background-position: -51px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum + > .tsd-kind-icon:before { + background-position: -170px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -187px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-interface + > .tsd-kind-icon:before { + background-position: -204px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -170px; +} + +.tsd-is-static.tsd-kind-event > .tsd-kind-icon:before { + background-position: -136px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -68px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -85px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -102px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum.tsd-is-protected + > .tsd-kind-icon:before { + background-position: -187px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum.tsd-is-private + > .tsd-kind-icon:before { + background-position: -119px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-interface + > .tsd-kind-icon:before { + background-position: -204px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-interface.tsd-is-inherited + > .tsd-kind-icon:before { + background-position: -221px -187px; +} diff --git a/frontend/docs/assets/icons.png b/frontend/docs/assets/icons.png new file mode 100644 index 0000000000000000000000000000000000000000..3836d5fe46e48bbe186116855aae879c23935327 GIT binary patch literal 9615 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 NZmR35l_G4O{0}dcmS_L~ literal 0 HcmV?d00001 diff --git a/frontend/docs/assets/icons@2x.png b/frontend/docs/assets/icons@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5a209e2f6d7f915cc9cb6fe7a4264c8be4db87b0 GIT binary patch literal 28144 zcmeFZcUTka`>%_-5TzIqq$xo`r3nZ`iiBRG(z{ZnN$)K|ii-3S5u{fmRRNLEoAh2n z@4X|01dtAA(50@mzH5K?{+)CF+}EWTz2eMdW-{;n-p}WG1C$hCWW;pD1Ox#ad~k9g4`y4!oVfq@3c(iW~uhy*`T7_0aH7`>`EnYuXVq#+YC==3#rnNM4TqqzM zpi2Elr!3hl!ZdK#y0bV+yVc8rwFEtAX3=QlvJ&e-EsBp)Q`0yKXbNuf-yYw7kh0CD z|Flk1UuHgvoR+*QR0ee&IDUfUzE7*`A=P$6nC;BPI@VJs|F#`Xc>X!`<6%M7XXNok zw^unt1h0m>-&2{GiIGsByulr92XZRrazZs&&M3jJintF7A}cE^uW4zt_r81yHt1I! z6-_gmO@78G3$})kfyhR0^qk?zev_%4R$qSjQI3MAg0)9EM#TOAD=_tf(*)S$7yiiR z&5v>wk3Bn**iD9S_I#2%^vi(^O+gpv2i^A);6^AcH%VC>0nH8|O!jN*L<#RtT z@aF9HMNu*d(BdiZq(LBO%(qsjSot+ZXQd{zLYh#CvOrK(?#u+|XYRylqcXOLk=m!) zBp`~~1dg7kF(Q#m)I8ZHMOD5%m&U)5jGOW@7+sm1N+O~^j*zRG;e4x@OteV=T4yo9 zSG`^0j^S)ZYp2DT>}AR|n$S)4FPI#8#(R~;Y**AZ9`&yqT;p`rks7Nhz;)dn-TgXU zw!^Bo@W6|jfp@}ijsSEFo#x3LnG;`o_yXK@2KuG8cTv&K@=dU?_PK*6=YU9!Ix8l;<_!y*Qc2phVpLM}&t|CuHBv&{M$K?VXtTabi(7kUMwV zl!>5cDNNqK6`Br*B~EcVh#5Z!FgiJZBN5nzpC7?UdAc+&AT0ivd;DA2$@YXMPK6=< z+#U~?*!R0i`3uu|#zDrRRN&j-j>ZOu#h-n#7WO^)@0> zCT6a$LGWwFLcPfN=(3#6`*UIS%uIT=LIXV-RbGE&!!+8)q~dkx`l{aKCe1`{J<5&< zlhRo;JX-UC>5)X;mwR+W96`@&ucHp$jIb~B_w_=mH>In?BLume!Wta=`ca+&7~pek zBVD?f5{nelCaje~EtZn+g3%5GJF}R_b`q}IH$Iom2IRD$^h*R)Cid8Q5~4Dzm!P&Q z<`iI)4wA#l@TwjPL)*9k5Vc!!;`9;bf?HRMm86wi9LI8A%*NGep3g11H{aP)>%l2Q zRMMQU!*0J$hJI5Qs3b=6?}qR7O;BU%Yzufc*ZKBV`}ro7zm=C?OY6Vlabc^r6r7P> z?1c^jD{e4n*Ou441V=Pd1eE8utX@)G5gq72HQAXLZ4l2wKd@yIYC+s) z-mu`E`kj=B!)a^B;pecv4W5oh>_tpj>^NU8L*eH4EhcOxQ|);$x(z(Yb5^tudSptV z%8z{(h@_t`chWkvFX=r!p~Vjhf1AdM>uGK05$1fyLb5D7m0!MUKW=JTZv)bXz9~*F z$yP@U3UE0=$;yjWr8b7C(1^oNDMZVxYYeMtL}ZnvQDkm>S0)=r_ugabEZ}AJ<<_Fu z{I^KKIz+V8K|pK811W5r##z8^S*2fr9Ln zlRG?Zzz8;xu9VSE8s+=(!^TGi1P2hC7%7MUqF=cZqFBtJNW9BROV ziv0cjsUmVvsU^X!`1UivK|dy+fSG$3YH8W0`q${`)taBT9jV{Hfh|&RIaJVvqRIFh zC*Rmvl&3*;XcMiJZ-+Mvfe0xN4N?AvJeABnNdgs(BYb!fK5<1)5UvM!Tz4_aojmUX z#Ymoh)m%fN(>6|#*RP~Lxt1?5);w}yT_lftje3sidO&MxNgcMg9@S+>M%s~y)0i`8 zT_+7LrZ~d<7V^K^C^~ast~@nM04^c5dw*&660^p%^R>n4xzd&jo)Y@ z1r=F09>jFOr%wsj^a3;>N!{rvf(qpkAdWM*5IYCsuwNwoJh7;9I$#`T6-NUIEKsiS;OylQ(XY zQtCiR1dyEGJV=~|zaFOEveB&szAVx*wsyuY?hiBGWR{h0!D zv;G`;F9cnib*YxugasrI^%uy@i)>BvC4V8@! zwy5#iHC#Qar(i0EPA3CuMQbaKy4m$CLjLSNwJs!13b%h{&x7479bv{SjC&3?SO&)3 z6q4nRRP(zOfw-mQrmx@Z64~o}GNXa9YCE$vD-(CLseaF%6HH+WZz4 zbRiJ~zAtA6*i9;z!+zZ?9~V0Lr66|Ae;}U1e#6D^hMhB6XJNHZi{t>DgU&jb=#rPK z@s04Hr_SOr%UCRY_SdDuSw^D*Rzre~4PCqgc)DBYam}@G^TxsTqX%w-yWtYU-Q2IX-a2Z4Kz_-yIe`m;x2bY1F?XZoIH=`uW{$R)ICXxqU$- zG#M6s!fDZwUOA_cs|PXe1T@XN3^UdYyR*t}943A1dTvXp!=%8c%)(s)5y@OJ@@%1a ztlq}Uvhfo3^ZO>ZO|NKfu37JMRRmXfJ_*VOBVnxFFmbq!zc%A+R+w|={11?sJpmca zCeCi;;-*yO)ywzKxa#q?E%@U-+LGH4{=2|reRd-Kz*Ps1$u6sPFO>{K9^k2Y!@=h7rZt472^BCU& z|0MZmbh1HlC3#bcjoX#m73R?H>6oW=45{gu0$S>j`v?``ch#0kGur}QbO_gO3XrB- zS4pz-Yrnqqt-k_LE-&~ox9gd#^n&HE%Z~grM;N@Das8-#U304PA$v*rj36j~qQzYN zsX>8?%q9DhpxrWR@M>30YI^WUDh4bcn+*bYn;~zt_g`$3{#G+=lBmWE;j}5e&vlDa zjsdE(Xg^o(Z|3$Tx>~-q5NrZ}^$y0eMd|h`7Y4OWkgF0(Cu&CfJV03AKfzSGBhMU4bqd4kc`qE!CH4Q^FdOCtUHaZW3R&>S}$! zhk=OYL~3fch$-?wa0)OEkynDzJR=vc^vuUQ$hF(>E(q3{7{4uhC^f@bzHUZT>k%%R zsekA}E`OlGE(x+lP1smp0;Ba7{C$F=@Pp~i$AsJkc)x+3Vf9xQB=aSN>D!T;Y5iU~39#6yoQuj6Bj%kdYC z`72YjnSoF_A)d#@S`|;~F|6TOn%b{4?MWJC4uG&NK=D zqd0rU$A@62MtWD$=Gg>TgO6)b6Vf41#Au&Zq<@p1RG!t}NG8kv#>%{bHuCdAeIao2 zkWX{dyO`XCdv`FlK?jS{48~Uaz;oD6PtoFF0u6HBTHCHh<)5wP<r?9UIw%{psu)`l~*PK0?1^oH}d{D_wF{En-ejdBHTK|(*2$K?xVkG zwYXl8^HAjVOqKQj0f6s~O`)Slp+alXd8@#4Iw?pHys|MW1|l%ipCPeN)|fLB$Dc(9s}LNw@?8G{ zU>U(Vid5}ltIy~zNv>o09)rC()g8O`<5~!qF*Z_?L;+2Sy!WSv=}|67mnOPb!A*2; z^f>okkk+f3+9?Tg&6NBMX%;BtB3Ds#(PZ6E4`X0e`~amc=9QGw3J-$!nw6)l1A8;m zFdl>D?g@J3P-41+3N`R32d*Hq0GWj!{3n&rVA)dpcB+|5`XZFFZI1bKA7d;-x=0wt zy;$6nvCJ$_&JDjWa%`LQYq&(6LqBP7G_+`+4$|qk7IlS4wK{qnP-3!yFO%_fw(8(Q(#|htD?ECEYPeT&anf%0GjGQC<0)vR3x=4pq`@gX z{0?*O(e3p_zu@N9G2O%!F8j&|FRhF(c@BWMxZTpdW0xv^K!`2L39%+Hs0#R>a@n-J#u*kF6~?DIhPrUi@$pR0tS?5wF%PE z(-eYCc#{7tVRzd>j~xO&LBPK62xxwmxrdd{N6!G1hfD0H?fV)_B^PBIm|@~CZXnpdaM=<+?&D8Md^RL00JfP zK|cm@`4bB6muuN!Zck2>k+wh^8kM73#1(%6#^TG;42H{?eTC(h^zB32g{Skc%t3Dn zcHX3$TQhR}n9xXCd$?igvlBH@ZU~p4OO*Gf=$@=w?9vYs)!RYa9V@}xVt8Sr4y_!< zGjn5?gnlSKhqS-YW^o#@NScez6I3x{ zv>meTLLYSK!pa+|kqQI8rWST7_)jL~mqQ}Ou*!V2U-g|ZR+pB%Z@w|HnZrV~uY*w?_gMhSp+4fY?hMmdNXYD(iruAlj0&qga8nQ1=c#y* zgYc@oWp>=|LQ+s})zQ5kv*UF?QMJ2|FN1CzjX$x&TwGJ!4VjOiZxVDVz#r28{^WRn z{o1SYRs*^Nt9(ZX`wad=44v--X~h#aROW$yKE=n-VWRfhI&wn|_X6(` z_WPK(bt4Q8gxJ=b%BW_nNj&h;H;2z`{vi`~)tCBk(zGYBp?f;(Ua+^@+rKm53ld9S zPP#A^Wv7>F7c36IAp7(%S716|mr9fnL?n&Q*?OcmX7>@shP*98yVXmJ{1{z!s;@_D zt0}M~j-0t@?)wY>a9PxzCVtBiTKiS1<;-&hv5CHiv=8d$IOnl?aI_>zR3eW}l*}`T zd7%jWK1w(iqAjU37u~dz-4@O^=PWhD7_yL+z1;-hnPx|je;QFR?I_x6McEg|;`Zuf z_}_7>V@hb=%%^H&>8W{N&Ud5bKD%p(B6#&l@nN^wOdQizb`@g}g1c|qGqGr^c>a1w z|5;G!BbS8(8#mlqM+re6&;L0Ba$evPxRGW!koG@-z@*c+8&^U^7Q+0jgUtgB$)Bh)OGD5oa(ju zL&w{}@q-4qVXtvRtXul%gWH0DxXe$&?MN>z2jh1!ElU%a2;fz@xaTyfs`lnr<` zLv5teGAw`KJIh))Wg8JzoRNMyP>X1rhr)=#Y8O6Nf7>}xLS8!@+&6k0h#H>Nn{`&~ z<h^0MI*wtWWT)UGMw#$-to|sCF?yXL$;_=8T>RsAI7ks*W{$R-UI&M5a3{Gda?9J z3PeWSws3vp1$(`F*+<1X7B6hG<6u)lqr|?N&1Up;Si*MeoRFeRNGZa1=`C?4ZaPvJ zuHL9EQ^d$jd1pu9n6iBgWPMtJyxmfJGQf{a*eag-%E@KZ$^*2_&F#h|LL)2_l*QS9(#5T>)&wtE8a=@FF+vG8N zk>*kU^97;}tRP6EGf5HKhlr6@^Nb7N1`_>QnnYF9-8tncspx59kcfE)TtFun#cCjn zEU2;}6Xu~xx+Bv+O;tKLcuo?~kQbcPghcWdz4-^H!wQOhQukRZRMRk>kfMa~V;A;p zSqpR3D87(4X}j4Awfr<~7h4dgK)pzpZf{bn z^yt`yH4+85n%*$3rL0fWi>l^4|J{Qess(a2+0W-O>gl%xIaVi`l9N3Nq}{$Q?o$#6 zP(6};On20~O*x}!V+=9YO)zz4yeTv@_04tEzA@Muc((5aTR+rHpa6@RymHX{a%Ss{ z+ZVey@TSCpCZq6G3WNWPfd3Z(|HlaUnQ37#)!hnd5VH}%lQbK+^qVrFox87bV{eTd zMjY@0wT+?ndYzV$vST&K{gWpow&Zbq;%=a$(B%@MLh@v!P|L4U zgM9JBN_Gb)g+}3@K$8-*b+GGuC&@6v)Fomd?4){kVQ)620*%U<8saNfLM+ndN~1z> zV$;~rU}Fc&M@|;i!@q(ZqbHdoB(EYYOs>u5jd5A-M`}}pr;g+_B5o2kj-|Pa zF8qc!e5d+kUV>;ih=57(*r24g=6@)>+c%LfGLw_-Bbm7r_`az+tag}5rqG&jrg(-W~CJFkaxZTf@_Ofx@ zzxqF#<4|HKKBpc&B9R1r8t{!k_=WNfzbR?aogs939=bT|!c4N>91ai-wsc4|JdG9y zGpB1A4i1ueuSS{R3h}0^YLpx`pB;Ok2-R5 zZzHya))4+|xc0QJ*&1>3;@0$RcgE3M_rt55cZ9<51j!pV&i`8js3v%e$CG{I{X+yj zruhC$iN%UA-Y%u_?FQq!rBg;{`8h`ZCg^bG&OC=733*%4cUW`DPGqp|OgNy?)-Lky zuY7>yw$@M~Jl&X?9MI2RqOdsWZwzFd6{P)UF5-=GVh z;$}}BvAUMs#V{T@TweGxI7dhuIzFqotm&oQreos6)^Nt1G4l8ce%&u1F<%WFM9t;W zBAEtq#1FS}e7Gq{9nzJ-0@1fhx^+w)&5)h+@I@?kv+h4xs>`xqTMB()kR)QH0W6ODL=b|ea)CmcTzPItT=KH66{L4@p}bW9=F z=+(cM#QUgiq$M^X08=_kUPU7sf!8j#4rN7NO0#TX0-;8=ySO&T7v$C}*`++cHZu0; zRv+{Je*j9;z>+TGv1i76Qc^1lu^>XXp&w}t;MzI_nTpY_m?O?J|UF!?x>j)zIZZ*}uTg|S?56^~@P4iEAwq#7&c^D#OmVAeT^&ib{UcAER@k$$X; zQdR$NNz=G^;6|aY!VuP>0e2>_I^ymyjmC*~Oj(aU>lb7XxoNc&mR~HbdffiYw#m3DLJ)nb-vczmSGI=PaP=yOJ4mrW01pSsP02=(ym z!R+#8VFsL>Puje-hBZZ0gY`?oFt44R6Z--pJ~w8q7te$W<+z`WB)mKtrOR>%f~{*2 z8>hh;3|%NPQq8-xDbWw`*n5*Ni7GB0zr7D?q`b1s^a4*X%Jk>EYA*r$va{t*S$Wk8 zL^lqaL9$a?PVadKA#e`-ocbsFKC1awpXsVmMxs^Fnz9Tb*6tD1sa`;k~@OqRo@ub(|hVwu)j^O#EQmIetE!ma(-|!O<`ZRqJb<$^dia$W5ARK;F@n)=G zXY|L|OhQ88G?ay6&;=(qqYF;O$NJ7x1?PPHYJC`UButfql;CF9^Z@N$9e`rgvKY7- zzkY{r^gSjplQ4S;+v7}YOOB)q;im)xJ8Tb}^>Fe{+E{o<&QW1zc~g`vO5=ii`UUW? zZp)~%d!YRLs1P5Gsp1zs3gc8)u&mU&?P*XcG+Tr-__K7L+$}7WQfV_Ngi(tq_9feK zK+m&sYg9Dt?NYYIX6$uOy3OW4i<~fWv+Cf(7LSO2Cy{IK;1#Y8C_5@I{l+TY*=I|v zB849$N`$Qn3)Wezrk#N{(Sj^ujO*o{#sa4oD_O8zmLim4B{5HQWLd}YpB(b z4G-q~15C`KQcuBSO|^7AHPTM2RneHT?`cv7UxhiJ{_{;Q;kGe05x5xg&K3|_>$pD_a&U>aXaI13$(JL50d8Z5nu7>Swu zA*$V;mYnn2)kI5c`a29y*`L60#8U8YzlVb^NVbZO*AIlUcC6{g-vYStoB)oYa(>HrRpU$_+Fu$?E^-+?mgq9i+l>lZ?b zT6(Rs*ytr2RlqzPAC<(}aFaO~EuqFiP9Nk%5YV?9#t-?A=4jtCuRhpfZRc5{uXo+q z=LI8vUYPpMT}NAmAiT1T|Lra-gEjft1a;1k`{Oe~KvJy%Wz~FR@vzsl)Hj`G)zsap zD0(^YuCzHguv&0Ryn%gl!eek+ywQej&`(Qef(ql7EcAYQoG}tAUY=Ns0uhUO05V)*ND z@*NLrHqhR{%JlU-nMJbBbn#Q$0gDOt;1glG|M6dhX@zoq#PRvcMk<`}n-dBYPlDbf zY2&o+<&J4^>4Q557tWSxa)1M;mS}X$!JFe6+N_0AI?erp9CdjDGuyvnelpc04y2u#n8-PU5wo6P&9?ZpnONA+t}Ucy z&nD(V>H%M8avRC7jdV$uW8n|L5W6kw7|(e8$j>_ZLqe`6y!1fWM}{tJ3t7HmzB894QuSOpNj=&WDT3e5Or0)3wFwasb4%9_M@6)K z&l3J-@<{!8U7lZ%P!XZsO|ejU04NSjBEBESP4Ff6+T}!&pxTCxBG{W z{I$5gyC-P##k--2l=5r77AsRg@o4?Q7zqe%7Y9-kbSnK|KDcKK;nZqb@o$i(QzUtW z4FlkIku@T67|OO;)}XWaHSwT$i->~}#O|Bld^q?M%%`d*s2x9BKP zZo$OD?q27J1NAg#Nd(Fn?4I|PbI>nwdR&!F6YOHC^L#n$QG{zQGnjL8QL{~TyS%sy zMT%4c%BbJPXL6?WNg|O1-c<>qUm^=RW`+5)eH2jAI{T^M6-_natW57V(D?*MKT4n;I#vjkQ1Y~X{0hj4% zF}qYRzy8zJX(%d$`X$XgPvDafqM65Qw_;|~(JO*m8-*q1ir0~W4cd`@#KX3_GEp5t z5?rPAGz%$L?%(5dRFgw~R^|tdxXDGF>^=J2drvtC0;nBNt)$2d+>6A}c}i_~ef`fu zywIKq{Tp+H@09h2i{+Dn7?p7~8D%gZ+<(bq<1f|tL;Qy~w3}O7WX))3Ej+(psj!1- zrlt&tNKU|u?sySN{!ByuYY@P5bL5@7&Uld^k~iLzJaP7WDAI|JZrsHHT>hmAC?xw& zC!c!IBNTzL7K;wAXR3vVTe1i(oYdqoy3H0Zw{@>?*4UcFaMCNHwib2efs0(Ync=2q zwM72#(Cn=nv2ablw^j({)fdng^E-(uP|5UD8@CzqpKlZ^=HH}?5{kmM7vLAoAatc; zwH5KZJkkdhh8C1p5+HZgC}LE+Xu}KIn7|*#?;j-8^-VaZ5jOW{JA#*;g5p`(xTiDd zKkPnW*IU@QEsE%-JWbaZU2+aF3<-bfklBU}TCC{E-~c1suP&!}=v`e&X_xF{wro+L zcgxt?1af+ArOGprbI<(>!E99@GkN&7?#q=uz{(bMN@|0qqxcTr07b2;i>k6W8Za(r zOGe?77{mF3SVV_<+hIDRNdbE)(lSDJU|Bf|swOh*8)pQ6AizER8M>1xnN1+Qcqhg$ z&ak{6PD5v75^-mAcvoOH6*!9Hkzpt)*#Ip_vNoGk)^|nj*9+w7+7R(=j4q>aw<4Wc z=nBx)kd4$ER29&>bnknJ`n4)pOczJMPJ! z0)p$AgO&S=`T1(PYN?P}4cSJ%&R?iNexQp^N$*`-AbTP7WfZIW#P4d}}S2|=#O7ke0mzh*aEWQE)y!|#~iGCKXe zpzrFFL$pk!^d8pUI(IfGO<%TTQHsrDXLDNnMC6*d0wT9m7x6Ft7V=_OlTqkuj{x>p z;1kpB_NxE04RdYk)Y!laqUU=rfZJ$T5)`7`QV?5(Ltg_xlECcjtEa{J!@6Brx);>b zl?P)xrifEIfWi;~!Hgrq*7bz~i3BH#^2_mOIb$vnOz3yqef|S?NrX2~aMzcrlIGhJ zJ57YYnbrjk0gMXNJsZ;3!GV3+U0eN7l{dNPN>2^D{M%{F_n#@Jh)M2G9pb6tlT&F# zzc){OFWO&LCDH1cNMGR@X9VA+vt>EiQ|#sD{Y6sIh0eE(T5g#Bhn{L{CgdEL#dtrL zC>~e(BtwcN6QdM$0h>v5cu{@BvleO1d{z*-w8N(k$wHP$AXwvfT1)EL-?E&6nLdTq zFA@*HmwLR__b301zkRRgd(MeG6hCvppG6OwFv=2NKQVx_rQX$Z3q-DFDcOMHtbuC2 zb}=nSGqv$BlXjj(ahhid7ECVPglKaK;z#;LgZZ+OisWYuKBPX7xpErFk*@EYkKqg2 ze61oYkPXBN#&}jK`c6OUoF{pGlCOmyvi0VbqIH)+GaMDJ>Eg{$20?GwP~=nbph7n3wT-iS@IWTjG!q<-}5nJdNKFs75SDJ`2N60FM#00h+c!NU0ufy*_DlHj73t z5%X`Hqe$xxtHUL9%+{FK#XTYqf1a`&Lh=``4pOX3cy239FO^N zfStakz4XYa-?AppcGY?%Pj@WYmLvxBlKhq06UyFTy`Dj|YO2D`3uG#B$$f7PEjp~U zN;XAx*Xx;j?A}%@n)?=Uw67Bf^MPlLUonDdnT0whr^OXyCbtVRp^N&tL4I{~Dg4l+ zvxK9}?_3)Y$>n?i!054VsQ<#MMZ=Q@luen-sz=N_VC}l?`zNJtA`krH?K@>?REBq0S+(}^2UlFWDqHi30Pa~uu05d$T+-JrcJV1?aXOg(}Rs zl`@li5%>|PHxJjZT#h6)u5#ukqU%dvk;$HYi|x;L7naNA&)c1zj7(iIm+BYA&tK7r zwW0zwzaX`x0|CVQVi4}J(N#ScVIBUXBSyY%CN{!aH)SJ(GEwpFU}-yF{d#w05hL=m zqA}!Sf^U&%EPmu~34)ZMEMWZ|Z{ zf+Da%zhehlo-wY?=x^Nensm)O!dR`~B96^wloNE6>dRY#u#pQB(ftm&2{0{aPw);3 zLS~XJegtuFdsZ#-4}Yw<2z1ya*ZublDU*Ut>&i)(l$<$AW-E7gWuf>Kh>nR@=~Jgg zYVeI|2kH%1E@)ScwTRMO*HTWJ!AcdT*o-xoiH_PF%JHNE29RfRx{{W~Mn)HwZeR53 z{~74suQ)4?@;WN79bIYU3yi%hNhnxTu7in4w>kOLA9 z^_cPfyxl`BO^Jaqzdl`|Ez%y3HTE#{dbqX?j$5k&zQxN?z*CZw+vAZV-WEk=-9oI^ zi>;EFv9pBIbUMsM{{@)yaWwa#nUxs`jEZa5y%dJ~ZYpxpbwF;r5KM9NBrtI6bS49Z z{7GcMaXGAxDfXDD;60Li!JF~fHPwUU&ynr@B*@3ChF52>+Zzj(2PL6C2Mor0xpcaX zJz8ihH2PY@>!))WZIW^vV%K*vW$Xw?vcF2|dP9n=qCP9;7B^IZhW=jxJ&T%Ztkc=ADNzA zsx*6uOG(O5$(&<*ti|J7dW)DtZjKZ4%;`A)POZf?A4Jh3X-N5M*8W<2T>+@m+RM zso4=f_o0cfhnM$+auk~mI=kVgHZ;l-+V`UB8DLApLi~fqxxCu82ZpTHwuvkJ zMaL0c$(fK#3^%@^>W3#TVHR`5ZG3y0Clb5K47#1K#yLmQyhW_55~ZZn&H*`)Kcz#xCRQCFdlucHx%dY1wZPf=tL$KK^-_TTkBlg%SX#-AMe8 zDRJaA`0SE_!0FPPn@x{0rimZQd9k+}88MLx`S?6fu6=l1Y@h3fs<=&*q;z=urTS=C zK%}u|(8k5e&Y-zSmoYb|zD$^cY}p6(t?!f9J6m?2>Tc-Xy34Rp*Ug6P;_=3oS~ z%u;Q7%I5MiGqZ{d!-pEl{0|+1NTm+haNN1M^6$Gh!|V@!B;}D{h3pn(C{xBk%}#IR zO1TK6*^j5|!U4^zB>Fw$Ab?>qDPT1M^Jx#~^C&2cPdIB_0;KSVNk9r$##HLTSD_Z& zz)jE%*Gj)7d9uVMl=+HdJ8%e}9%lwaY;_kEvV>UsLHx;mMC@f3lzq5Iv&y8{w)@Z#?E z$bXT?tyF)?<3bugVVY6(e@Vg`2i>|)$^m~$WioLwW}oXXZ}=w;=N0{LOx0{9*as^Bb{)>T@3m+vEip|GPIJDHTEO0j?I58}) z3~@%Q(7?0uCeHM#BsO=kytmWFVcmtD#HF#V$&{e5iF)nW6D|+WjJvd;&5ukcPLykI zL)z_SO#T-IEgtk{E$oT_$8EEJI%wS_Y2C(F)`01pzGC)%N-d}qrB@+6yelt`_?uuN zPMGYZCo678{Kdb+IPo{#IN(js1Ummj@!l19H8oPMb}r|M+d{D&z2T^r|!8rbRwlE=7j zz{QM`99y%o-F!wvWl#jR$l|ML^ohwPPlBQ~Vi{{yBOjvrhl~uf zK5Vk45;70o*YhtM&7#Sc2dfA3wZq@0ZZ6N~v6zg&MzJl<$ZNrwqf-$TiT@#W`2x6Mt;TiS4huyA5^}YIPTFF^l19VciDe9QgSuo770l zz$Fvs?0FY@_UtE2YE##{%dGmgZHHfzsU_`V*H`P4*F`ul(sYs9Jq*h6rbk1>eD34Z{2K;_cLbZ46halLc ze2%NUKU&GA!WwUqG&=coFm>87tCT*F4xGxo74O@5Y3xJVE!8F_1FP%~BdC2FS9Isf zXuW-CnGh!{^D*Drcrxc3Y`W9=5ZVYqn-rEs?8_&q}IoEx+VFS zRga(VCYV$<=Zq#wk?;b+las#o#HsNw*`FGFDeA^*xQuB(cE3~CcEUYt6MjgdL|p=P z2+pPgOZ0Zk#7FPiJV}Wb={;89-U46uTu_QI1&b)P=+se1|88_^!5Um>o)Nj!lfI}_ zA{$}3*734@W4yItj?m zLJCa$`Rn$L_lRPSglt!uro*Wg-e^WHi@NW8q5zxYdq%ULx=%RZ(Ry~zKFHmgD!x8n_+?xj`!7VyZLb@!Ht zcyvx*=Ox|L<#!iwxI;b}HqA-#(_&c7eI; zh0-~Nl>BWL;lGfbd$~ThM~0`;bnAxA&t^Bg46A9F67?ijVTmmSHXl37dKJH@X%pJ( zv;J34-$9e2BLwPjbgdS-#g6)O&a!wuZ-4?=C;(W1fb*oq3F7!&Q;TDT{dSIuAJ0r( zTYW}1z5Y^?(IYRkcvPK{&UNZ!DTD2NG^^l4v6pZ*x!@0~FW+zs*VWLZvD5?b&529v zzAIr#Blpmqud6Eze&qzM(zwET6WE`YFdmz$)SiInkY`uE9 z2W8d!Z|P-BLFnbp3rcnGlI9P_{}G(V#2CJpq^&-OF7u(-e@`ex!`4!J7AZxIWjne$ z*}p)Oo)D;<^YCfczySXZ)mxzJ%Trh$e@@Xs6YI$UjQXTpMM3=OD}yJh-k2t_G}69%^Fr!Z2HQA5*4M*x@spn| zrheG^IKj0ez3X@*QK}PLKen)$lLlOFZ8tSxuEOsfZ4ZBRv~f7a=7}eY0qYvDhVUkw zZOeCWJKZrO(yrm9v!+wYKhPp+8sVTN>nKBQt1)2z7ZTr41?oJxD3UIFa*^`;bD2FhRFQI1$)e-S7>YM&OE5M83i$Yg1gC4XbSB(3HY$XeKc0w~r|t-}85eyvq znGOcAFmP`I@uNFB6D-U3R7zi&HI?4$T$XBCYp7jyF2hIU++&75Z}~Yj0lG(o!Q{%x zle@H4z=iwQ^%fFV}$@P%l|Q*S||Fc=aU(OuYN7&dFa}V3Nc7J*3pGRNHysT zpl1qYqD}+z4udN>1yr0@uF3~3%~hGND|wBbU_IaPN$MmzOSBa(DV?!lmqJAFWhao7 z6XK-N{+v`HO%=al&V4z}>Sa|@+Qf8!nk9bZMS#vdzl+RDih{^-@~-07nqb7URdH*R+DD=7!&A9Oi{-a*?F%R^?_>z|&W zHQ+4C_b)3pp#^K(qJHO8s1UDOMw^aDYOOebgZD{HMbGVDVk$+=PF2;lVmdaX96DD( z2>^x9360&?xbJ=C?ww+GUzY7mi#yf$i@Zi^^Y}?DA8FLB1O|#d@$jX3gICv(QdzlV&8dxsHV(c+LsK>QTvzU6_ zYb0#5dCxZ%c~~}R7+|_=M1NiJ;GL(M6jlh!W$wT&BZz#^;TRxOvOoC5av{aK*jUdB zEJTT7g$OLq7j%VOxq7lBmjswrMs{Cq4i_QLuY?I-R*l_PX%)WEauEF6LE{{cM%g#Z zY=g9-pHTq4-?B_^ws)ot(CdUT(Q;?3ZgB%&0-LSJk}S~oODd0f;gmE$LNlWC)*SZw zTF2tWUDe>}3GAgFzfUW{@fr-5%+TXNF!#@u3xLK#M@{^pJ@RwHxR(mQv$rbM^u)yF zp7gc4+^-scO=w4GnLoUHm&|*G%B4)zdnT-@sLAXD{t?qVWoK?M#QmO7ZDZYumcROM zT0RXq?@|A$uOb2&0IX>Ab9ty?U)lM3)bo7LPM+d~0IDZ9U)9X4Pt|IhEccrc4$Yqg zxN&t9niz^0H@V{LX*57HW5=4LcVn`mZrtz!m-E4LWa#a&|ZE=ZeR z_be>uWC0uQotqmp(+ySAn|+s`Jh^?c#?)U-^^qVEROY9akEY4F$EfL{d=!)6%BG-- zzxb^*e?e$Rf1Wl1QT?k8F>OCoXwv?=Ung`f@oR`*z|{D)G%5h9(2EXaoVg^$f5Zm< zKZTunJXG!9$1R~Oja|ej${K1yXo$j8_FcA;rjQxV!J)?|Gj8yk6(bnRAXg-|KsQuFvOvU}1Q)$#BKFf7rFv3#c^C6nuM& zOO0Gft$Kq{^uZk+fBQMx4ywF#eZ10jN%@}^6Trc3hCtkr5v?qLPeTBZoa}i>5KfE4m^W45!H&tNIy2!R)_bi2pfs)oyorVbu+nl5 ziVqIJzcjU0;LWSXA>n4vmdvWwz`nJ(vB0=#2PO^BiHo&%ecgXrM@U_;#^7aMCflK* zu?J85J`Tl@CXG@Gz9}c1FQwCP4okOwbBpS37P8a>qfV`z9k+`X5YFPzTfu%UP!6y`Fvr_P9?4V5;X6Bf8{U9#rCkAZ zM&uVB!n66B@`9(+a&}!KKRfCf^oQNN+6$^tHoMIK!>*$7-0ZFr=x>*b-P5X-LgxBY zo2Ug*pNH%q>8qqJmtk=~7g&DYcueN3PcuE3&z~%j0gUYgSS9wn57tV0QdV~{+bxEnx{U^j4&k6Tg_t{mX$_Yq$xe=@q|jc4#`MB^ zJT!tidMB9LT+XqKk3JFN=!_dS0?dknKn##1>;EeT2o)}9LyEIBz=e4SFuw9d_vq)Y znKx|vFBXdWkaNz_)-AYMGNnQ9zLj_f%C}~7N!N>u)Lf+CfEIdIU7czh$QbcAide4T zZQJy*?<2fUv(SP%PV21I_X1kz7G8vO5oI)0xCIvcYt6{A`!}bwQlGSad^&0sE+dig ztCN-J!D2iYgG*FJ2{BPzy1^u&y=FXDd67a8y7BGP|L)Sh_Z*1ci7meUFD~utdnA|k z%FkshXa7&|yHfQ-cZaL9*88w++@nx&uAPsEVL*=wVw{~gi>(snR7!xUfN3m@nIRqe z$bxi@pG5F$L=in`nIEOo82`J5h_9j*7~_4)pr(1ea&G+SOCoJiMKDK#1^!`Tmo zu(KAj$s(@Ez}~eSFWD$y#q zslU<&-b60sArh0MhfMd8Ut(rM_CQZ8FfKQivy3;fi)0|#R9eO4o~zDAw8`&mCJBRl zL+V<9>B#dX+=Ch6E=t$PUla#aJlOiq<<`$o@7t~|m@_8YX~f5JPr8|q*x0k}KKaw) zlj4s{p!Bb0(O2I@&cJP`BT4v(=^IBCC}>G;6Pl`dvTGO(u1uHZFzBch#Oi5#?{oUA zMDhff&?FU9`${$qfOt^aXNUDLXp}!L8o++(*YdqI@rZ`e_9q$WGiZtk%BdwBGNUQLOvKhbHU?bZL0ypyF6t66gl zm;}?$LvW7=cpykxJulrHg1_Tybvk9?!FUgQFW7)ZjiG5RKh5P)A-N+a_IR~*prd%Jub(3dwV#iE zEZRnitmR!zrZDwcFZbI$fi zpQ#2NyF^|ZZxhg}_2{p|uY5RbnD8K6ZJ*(Qw2)?}wekp&yaRA|Qo#DxsS?SeI+jqSMG)is9$_pX3e;QRCk`w z6Eyf}-+>ptnm-5fB$ja02cI*FiDNlWz6!au(Hs}CGqc@Mmic~|=QFFJrG1@1hjtXy z4~e%c+1cVu*QrSvt}^-J7&3CYOFA(;0v#pDtP1!!v4p;BvW*`n{US>q(dX{NUrV`ti>sUd7L3MP0-oP`aRTgYw5brGKhov{JH8&ZnR)OJ2X6Hj z*N%E-g5%w9Tu(o3p@Ox209&F)dqM|)8ypzq@>_T7)U{4lXM#FbS?FxaC!G^bZMM9+ z4tmuQbQP|}fWbv^^L6{ks3C9Ej)`TTPs7Rx%f;*+b8A$!FHS$N0rHb7YlE-;Os=Pr zQ{twGcgc=sfxFbo@AZ<0v(i)mIIN>SayZmhz4f%!>5C|cW!)L%h17s1v)z*m@qbN( zLIG`HP@`-xc!<{bo61SZlQWVZ1OuYl!Sb-gF-ru;V-o?-65R4%f%6Z;4dlCb<*tm4 zT`7ejX`!VvI;>13$7YHQz%+8p7l(Tpo$_JB4f^W={o?Bv;zK3iLCjqj{gvE5lo;fd zHH{q|VzJ(ecLFb~dW44K((lhkhDQ$2inQ@ZcRq7Y>-^*1b>gOVEt)4}ovdHpbt^K@ z|3sf`Dm|bJwcZkK{pP34+PPS-&Y(HzYpQh%%*U0(ohJ^qYv&SPhZse79v3M#nTUb? zTTjUjU*9&)0S1{kUx6pKuPYG_c~z}evFZy5xUz{>?k8wd2OGRLnS6!W@2E;KWyJGkUt&UFTh*2NVjj=kW%jj~V001z!4 z=ACav4hf=_2vC25z)FK{a-HCIF%1b@(>NH^N7$**yWUBYO61yA32R`g-kGrQqT2&s zZ1aW~`>zx~03Uhl@0bL?Vul+mpc)cp64nzfU1rpi*eG&?8WU7Xl4Pf1!!_iKpK_${ zC;xLY0h})InNl8x8hkL6Jpz7odsa%}^mCw|17HWPhf{dC+kQ}x((i~n?<}jL=p9a@ z<9^KPtHyuVYuBL`*B7H;P2iVO8ICwx_P&$c40y;=GC7R)u@F`J-|`;#me&bZ9#xFU zJg^Th!=rFfc{Bw+ujIxWBM>U0T(6i0?6X&W^QWn?a#<*foA?<)RQJ+am_wkw5~pN- z7sfTpB>PChT4dEn1d;2VMl0o-hg^bZeAQZSZ%fT*?fK_jkzO;p1^Kn_+yjstFP#ra zNvx;BrMYSMj?`B;0sS zFuJaW4L~Ou?IWxSIxyrDP0$laaSx}5DtUOzHO?=y^m2JYfcOG)&~ws}entE=bCT7$ z=#rYt?lU1eR^i}WaqU8Z0rKPflqR^`l!q|k(Zo+khOK+ubx;hXEPh&3dhXVaKhK_5 zEWuW;iN*%L+&b5&xM}Dl-pY8w8~S%KsSYAxoEeE0RatjS6)vupzw^Mi4zR4J9^a9vEO zGsL1|=&T;B!-Hc|XANCOT4+&_Am}oQeN;)!5I#Ng%dGfD89Z`xzBJfQ5Uq?0g3AeUS9@IhE|>w~}OV)8>HvkoV#COPN{LT#vk8 zt2Z)j@{a(~lW*kv*4-rOL6sffa^(OAYdJ-0AsgF9gwSQe2wH&X@4yh*TSHt#%TNt1(?*1p$1*$&WoXj%(3D- zcQ5QJ#PkYUg9UjMs?vZCI$TX&{X=JmqECeM2>uCx|CpLx$`!gYuDe(vVX}YRkFG^k zURe>tw{_d=^mg9nvS?KtpkI=2?(iG$tPXR5QosdvzxGoCt z$$I=Gfzpq+2F3?10L^~%hk|tHo!byiu28i+0-PzrVDKCekd-_eW}(>Fp}Ancc191J z%LV{ozGVXd7!U|yD)X?cRj`u12B#u~Q22#>5x;tCwV54R+A8Kzk+(poe&f<5a*v*K zT2oU&Cy_LPGej(sedjw!v3{YylrY}sxYF)>cfp<-T!xEu)CFu&YJe?D)I%N!%*L!8 zEi#ZVi4r-oMksMF`zOoUUiq(+KVL}Vgk4zs|M2{i%LBzJSShuf5=6EJK+gfbJ})q= zG0GhyJ>s|)s`}>jgj5{06DiB8;CT5#UeEFuCDRNU65yFEh+SOUYPR?{idoz^hcctc z&442k_wYk5d(L7ZTKmy)4^n0o##7c6!_jl_B86&KbNSP0;&tq_AS1DeI66n%PR*pX zi2%0k-ZNP@3`AaRb)vJ?W}XEv*Z1a+PPd6tY;c0IY-s0=Iw-*C*soU) zC=bBofdMQRHt;f`m;%bDO+Q@6&hS8dvdDDe(V_H-k2t&!J`FL&9w2#0bHLqd5+>n8)4e;ua%TPUO&4#d!TjvD`IHe+m+wqABkj zoNs5r+GI!s>cQZx77EF%7%V;lk~d43R$%h9**@|sc6SSR>J07Anld(@sT0nyR>Qu_ zPhkc@Fj;M*AKsf3%f|p*H1HyY%3g7T%cCKt?y8k0=-`j0laL`{!mVH11jZ{=3)Zbo z21^05#asw*jiv?Hew&@KV*;teNz-jz?UZ2y0k!l8DBW^9Rj~0!uD>Ft|27Lg;_|N} z*?vvL_xnuig>$EG@^@kLoJ?zdbt0stXU1YVLJO_W zCv!h-*}a>}{Q3SZv`DX6-2%p&B;T>R%A72KsxXP5VK54m2trhI`mBmx(#zV{ zInu6zS{==2l?XBO^i7UsOK?Fk{?ekyEXECjxn| ze`kRpJim|8Q}?3d(XG1>vcoX%zs<(_g-QWYTElLe@&5AL%%^F!{2#PFiop zRz~d(ix56>b@e=g)qGNk>2`{de6Q_WxRCIF*6yQFR#bxy#Qy{EQ~~2n-V>tkL{`UY z&0Rmmuj2DpeT)jObl<7A@des_b`d1V25nwoq~e9M<^f>hHSU>co8g(*{m}-YwofiI z-mkS=3Wl~O+8MFVW{YqX8E6K**_pPc`QNK@m~X8Hg&Kle5qX4L!dd6!IWdLU*Nlkc zGiH(n$H6or(h^BfuCPB&?kP`30z;2(u1 zR+FQfD9dIbldYlRvSLo87bRrF5U656yei7F$Z+uFv&!-!9(3wD{QY)By0oUJmuQ{- zU}FV=;Y7LSZ1uxnRdzVY10dxWlIkcKoJet_HxrwC@n~W6^hFyQekJ5|pV<4XQj zka1?kZLfD%g`ld(`_Jln6>AAWt9jnwML-$NI@O($<9KJ{W`C%l?Zl4-L0J7Mr!-?21u}Dy5k;D zu}!eeZ*3?R;L}9xDghYu?{zNJxF-U5o>7it>+~T~$v2ua{;7P)^J*yJ6~TT02(a@l_L<@JIZo3wOYJ9t9BNNUnvpIZ184_1fah;Vh@r1saB z^4y@`7jq3dxmVlsiow+%)C~5)FovY6v>3pvw$J%t@r@7cp&Ec@j$@T1u-i81-!`X5 z*u0~!^hDZq+7k7};*;b~0?h1x(q(|(>8OIVD1hr(THoGWk=iwDyIPzQf69sA=(J+o zn#EcLV}QPlry2xM(Oe*&QuTxz|DO({_ui&T9ig&XSsUK?V&dy)5>MGnr6uw&*J)SR z4O5d0C2t!+(VG{Y3fFU3G4!F~;z`0^Zy$VT zlJGjGSF&$3BUtfc03n5Fp1KQfb~InA&8`q*1q&GG=||Hzpy6L2H1f*;LpyQht{w?} zDZ2kUk>FaSr)>&iD|Z|7sH6U!z%}z@JhB~OedrN<`}Lfq^UV}Y43>cn?*zZ0AOM2< zpX5w(`QSQaEYTvqHz~=NXHUjQf0o%dBkQfeAN31lR&xxOEgYHTdZp%bVXN280=Ana z^M=FH$n=5rl?&BI)^08Qe_`>YwGkkoEIR+Kv^%~Pb0k^b?3|sA#qp8cs#eTueeM2Q zRw=0&M&6mX$~YF!Y0ZBc@63#c7`f!9BKSXd@Voc{RoLU+XN*d^;RK${8T?=LBS%Bk z&gk{var Ce=Object.create;var J=Object.defineProperty;var Pe=Object.getOwnPropertyDescriptor;var Oe=Object.getOwnPropertyNames;var Re=Object.getPrototypeOf,_e=Object.prototype.hasOwnProperty;var Me=t=>J(t,"__esModule",{value:!0});var Fe=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var De=(t,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Oe(e))!_e.call(t,i)&&(r||i!=="default")&&J(t,i,{get:()=>e[i],enumerable:!(n=Pe(e,i))||n.enumerable});return t},Ae=(t,e)=>De(Me(J(t!=null?Ce(Re(t)):{},"default",!e&&t&&t.__esModule?{get:()=>t.default,enumerable:!0}:{value:t,enumerable:!0})),t);var de=Fe((ce,he)=>{(function(){var t=function(e){var r=new t.Builder;return r.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),r.searchPipeline.add(t.stemmer),e.call(r,r),r.build()};t.version="2.3.9";t.utils={},t.utils.warn=function(e){return function(r){e.console&&console.warn&&console.warn(r)}}(this),t.utils.asString=function(e){return e==null?"":e.toString()},t.utils.clone=function(e){if(e==null)return e;for(var r=Object.create(null),n=Object.keys(e),i=0;i0){var h=t.utils.clone(r)||{};h.position=[a,l],h.index=s.length,s.push(new t.Token(n.slice(a,o),h))}a=o+1}}return s},t.tokenizer.separator=/[\s\-]+/;t.Pipeline=function(){this._stack=[]},t.Pipeline.registeredFunctions=Object.create(null),t.Pipeline.registerFunction=function(e,r){r in this.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+r),e.label=r,t.Pipeline.registeredFunctions[e.label]=e},t.Pipeline.warnIfFunctionNotRegistered=function(e){var r=e.label&&e.label in this.registeredFunctions;r||t.utils.warn(`Function is not registered with pipeline. This may cause problems when serialising the index. +`,e)},t.Pipeline.load=function(e){var r=new t.Pipeline;return e.forEach(function(n){var i=t.Pipeline.registeredFunctions[n];if(i)r.add(i);else throw new Error("Cannot load unregistered function: "+n)}),r},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(r){t.Pipeline.warnIfFunctionNotRegistered(r),this._stack.push(r)},this)},t.Pipeline.prototype.after=function(e,r){t.Pipeline.warnIfFunctionNotRegistered(r);var n=this._stack.indexOf(e);if(n==-1)throw new Error("Cannot find existingFn");n=n+1,this._stack.splice(n,0,r)},t.Pipeline.prototype.before=function(e,r){t.Pipeline.warnIfFunctionNotRegistered(r);var n=this._stack.indexOf(e);if(n==-1)throw new Error("Cannot find existingFn");this._stack.splice(n,0,r)},t.Pipeline.prototype.remove=function(e){var r=this._stack.indexOf(e);r!=-1&&this._stack.splice(r,1)},t.Pipeline.prototype.run=function(e){for(var r=this._stack.length,n=0;n1&&(oe&&(n=s),o!=e);)i=n-r,s=r+Math.floor(i/2),o=this.elements[s*2];if(o==e||o>e)return s*2;if(ou?h+=2:a==u&&(r+=n[l+1]*i[h+1],l+=2,h+=2);return r},t.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},t.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),r=1,n=0;r0){var o=s.str.charAt(0),a;o in s.node.edges?a=s.node.edges[o]:(a=new t.TokenSet,s.node.edges[o]=a),s.str.length==1&&(a.final=!0),i.push({node:a,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(s.editsRemaining!=0){if("*"in s.node.edges)var u=s.node.edges["*"];else{var u=new t.TokenSet;s.node.edges["*"]=u}if(s.str.length==0&&(u.final=!0),i.push({node:u,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&i.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),s.str.length==1&&(s.node.final=!0),s.str.length>=1){if("*"in s.node.edges)var l=s.node.edges["*"];else{var l=new t.TokenSet;s.node.edges["*"]=l}s.str.length==1&&(l.final=!0),i.push({node:l,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var h=s.str.charAt(0),p=s.str.charAt(1),v;p in s.node.edges?v=s.node.edges[p]:(v=new t.TokenSet,s.node.edges[p]=v),s.str.length==1&&(v.final=!0),i.push({node:v,editsRemaining:s.editsRemaining-1,str:h+s.str.slice(2)})}}}return n},t.TokenSet.fromString=function(e){for(var r=new t.TokenSet,n=r,i=0,s=e.length;i=e;r--){var n=this.uncheckedNodes[r],i=n.child.toString();i in this.minimizedNodes?n.parent.edges[n.char]=this.minimizedNodes[i]:(n.child._str=i,this.minimizedNodes[i]=n.child),this.uncheckedNodes.pop()}};t.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},t.Index.prototype.search=function(e){return this.query(function(r){var n=new t.QueryParser(e,r);n.parse()})},t.Index.prototype.query=function(e){for(var r=new t.Query(this.fields),n=Object.create(null),i=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),u=0;u1?this._b=1:this._b=e},t.Builder.prototype.k1=function(e){this._k1=e},t.Builder.prototype.add=function(e,r){var n=e[this._ref],i=Object.keys(this._fields);this._documents[n]=r||{},this.documentCount+=1;for(var s=0;s=this.length)return t.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},t.QueryLexer.prototype.width=function(){return this.pos-this.start},t.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},t.QueryLexer.prototype.backup=function(){this.pos-=1},t.QueryLexer.prototype.acceptDigitRun=function(){var e,r;do e=this.next(),r=e.charCodeAt(0);while(r>47&&r<58);e!=t.QueryLexer.EOS&&this.backup()},t.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(t.QueryLexer.TERM)),e.ignore(),e.more())return t.QueryLexer.lexText},t.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.EDIT_DISTANCE),t.QueryLexer.lexText},t.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.BOOST),t.QueryLexer.lexText},t.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(t.QueryLexer.TERM)},t.QueryLexer.termSeparator=t.tokenizer.separator,t.QueryLexer.lexText=function(e){for(;;){var r=e.next();if(r==t.QueryLexer.EOS)return t.QueryLexer.lexEOS;if(r.charCodeAt(0)==92){e.escapeCharacter();continue}if(r==":")return t.QueryLexer.lexField;if(r=="~")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexEditDistance;if(r=="^")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexBoost;if(r=="+"&&e.width()===1||r=="-"&&e.width()===1)return e.emit(t.QueryLexer.PRESENCE),t.QueryLexer.lexText;if(r.match(t.QueryLexer.termSeparator))return t.QueryLexer.lexTerm}},t.QueryParser=function(e,r){this.lexer=new t.QueryLexer(e),this.query=r,this.currentClause={},this.lexemeIdx=0},t.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=t.QueryParser.parseClause;e;)e=e(this);return this.query},t.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},t.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},t.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},t.QueryParser.parseClause=function(e){var r=e.peekLexeme();if(r!=null)switch(r.type){case t.QueryLexer.PRESENCE:return t.QueryParser.parsePresence;case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var n="expected either a field or a term, found "+r.type;throw r.str.length>=1&&(n+=" with value '"+r.str+"'"),new t.QueryParseError(n,r.start,r.end)}},t.QueryParser.parsePresence=function(e){var r=e.consumeLexeme();if(r!=null){switch(r.str){case"-":e.currentClause.presence=t.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=t.Query.presence.REQUIRED;break;default:var n="unrecognised presence operator'"+r.str+"'";throw new t.QueryParseError(n,r.start,r.end)}var i=e.peekLexeme();if(i==null){var n="expecting term or field, found nothing";throw new t.QueryParseError(n,r.start,r.end)}switch(i.type){case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var n="expecting term or field, found '"+i.type+"'";throw new t.QueryParseError(n,i.start,i.end)}}},t.QueryParser.parseField=function(e){var r=e.consumeLexeme();if(r!=null){if(e.query.allFields.indexOf(r.str)==-1){var n=e.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),i="unrecognised field '"+r.str+"', possible fields: "+n;throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.fields=[r.str];var s=e.peekLexeme();if(s==null){var i="expecting term, found nothing";throw new t.QueryParseError(i,r.start,r.end)}switch(s.type){case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var i="expecting term, found '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseTerm=function(e){var r=e.consumeLexeme();if(r!=null){e.currentClause.term=r.str.toLowerCase(),r.str.indexOf("*")!=-1&&(e.currentClause.usePipeline=!1);var n=e.peekLexeme();if(n==null){e.nextClause();return}switch(n.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+n.type+"'";throw new t.QueryParseError(i,n.start,n.end)}}},t.QueryParser.parseEditDistance=function(e){var r=e.consumeLexeme();if(r!=null){var n=parseInt(r.str,10);if(isNaN(n)){var i="edit distance must be numeric";throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.editDistance=n;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseBoost=function(e){var r=e.consumeLexeme();if(r!=null){var n=parseInt(r.str,10);if(isNaN(n)){var i="boost must be numeric";throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.boost=n;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},function(e,r){typeof define=="function"&&define.amd?define(r):typeof ce=="object"?he.exports=r():e.lunr=r()}(this,function(){return t})})()});var le=[];function N(t,e){le.push({selector:e,constructor:t})}var X=class{constructor(){this.createComponents(document.body)}createComponents(e){le.forEach(r=>{e.querySelectorAll(r.selector).forEach(n=>{n.dataset.hasInstance||(new r.constructor({el:n}),n.dataset.hasInstance=String(!0))})})}};var Q=class{constructor(e){this.el=e.el}};var Z=class{constructor(){this.listeners={}}addEventListener(e,r){e in this.listeners||(this.listeners[e]=[]),this.listeners[e].push(r)}removeEventListener(e,r){if(!(e in this.listeners))return;let n=this.listeners[e];for(let i=0,s=n.length;i{let r=Date.now();return(...n)=>{r+e-Date.now()<0&&(t(...n),r=Date.now())}};var ee=class extends Z{constructor(){super();this.scrollTop=0;this.lastY=0;this.width=0;this.height=0;this.showToolbar=!0;this.toolbar=document.querySelector(".tsd-page-toolbar"),this.secondaryNav=document.querySelector(".tsd-navigation.secondary"),window.addEventListener("scroll",K(()=>this.onScroll(),10)),window.addEventListener("resize",K(()=>this.onResize(),10)),this.onResize(),this.onScroll()}triggerResize(){let e=new CustomEvent("resize",{detail:{width:this.width,height:this.height}});this.dispatchEvent(e)}onResize(){this.width=window.innerWidth||0,this.height=window.innerHeight||0;let e=new CustomEvent("resize",{detail:{width:this.width,height:this.height}});this.dispatchEvent(e)}onScroll(){this.scrollTop=window.scrollY||0;let e=new CustomEvent("scroll",{detail:{scrollTop:this.scrollTop}});this.dispatchEvent(e),this.hideShowToolbar()}hideShowToolbar(){var r;let e=this.showToolbar;this.showToolbar=this.lastY>=this.scrollTop||this.scrollTop<=0,e!==this.showToolbar&&(this.toolbar.classList.toggle("tsd-page-toolbar--hide"),(r=this.secondaryNav)==null||r.classList.toggle("tsd-navigation--toolbar-hide")),this.lastY=this.scrollTop}},I=ee;I.instance=new ee;var te=class extends Q{constructor(e){super(e);this.anchors=[];this.index=-1;I.instance.addEventListener("resize",()=>this.onResize()),I.instance.addEventListener("scroll",r=>this.onScroll(r)),this.createAnchors()}createAnchors(){let e=window.location.href;e.indexOf("#")!=-1&&(e=e.substr(0,e.indexOf("#"))),this.el.querySelectorAll("a").forEach(r=>{let n=r.href;if(n.indexOf("#")==-1||n.substr(0,e.length)!=e)return;let i=n.substr(n.indexOf("#")+1),s=document.querySelector("a.tsd-anchor[name="+i+"]"),o=r.parentNode;!s||!o||this.anchors.push({link:o,anchor:s,position:0})}),this.onResize()}onResize(){let e;for(let n=0,i=this.anchors.length;nn.position-i.position);let r=new CustomEvent("scroll",{detail:{scrollTop:I.instance.scrollTop}});this.onScroll(r)}onScroll(e){let r=e.detail.scrollTop+5,n=this.anchors,i=n.length-1,s=this.index;for(;s>-1&&n[s].position>r;)s-=1;for(;s-1&&this.anchors[this.index].link.classList.remove("focus"),this.index=s,this.index>-1&&this.anchors[this.index].link.classList.add("focus"))}};var ue=(t,e=100)=>{let r;return(...n)=>{clearTimeout(r),r=setTimeout(()=>t(n),e)}};var me=Ae(de());function ve(){let t=document.getElementById("tsd-search");if(!t)return;let e=document.getElementById("search-script");t.classList.add("loading"),e&&(e.addEventListener("error",()=>{t.classList.remove("loading"),t.classList.add("failure")}),e.addEventListener("load",()=>{t.classList.remove("loading"),t.classList.add("ready")}),window.searchData&&t.classList.remove("loading"));let r=document.querySelector("#tsd-search input"),n=document.querySelector("#tsd-search .results");if(!r||!n)throw new Error("The input field or the result list wrapper was not found");let i=!1;n.addEventListener("mousedown",()=>i=!0),n.addEventListener("mouseup",()=>{i=!1,t.classList.remove("has-focus")}),r.addEventListener("focus",()=>t.classList.add("has-focus")),r.addEventListener("blur",()=>{i||(i=!1,t.classList.remove("has-focus"))});let s={base:t.dataset.base+"/"};Ve(t,n,r,s)}function Ve(t,e,r,n){r.addEventListener("input",ue(()=>{ze(t,e,r,n)},200));let i=!1;r.addEventListener("keydown",s=>{i=!0,s.key=="Enter"?Ne(e,r):s.key=="Escape"?r.blur():s.key=="ArrowUp"?fe(e,-1):s.key==="ArrowDown"?fe(e,1):i=!1}),r.addEventListener("keypress",s=>{i&&s.preventDefault()}),document.body.addEventListener("keydown",s=>{s.altKey||s.ctrlKey||s.metaKey||!r.matches(":focus")&&s.key==="/"&&(r.focus(),s.preventDefault())})}function He(t,e){t.index||window.searchData&&(e.classList.remove("loading"),e.classList.add("ready"),t.data=window.searchData,t.index=me.Index.load(window.searchData.index))}function ze(t,e,r,n){if(He(n,t),!n.index||!n.data)return;e.textContent="";let i=r.value.trim(),s=n.index.search(`*${i}*`);for(let o=0,a=Math.min(10,s.length);o${pe(u.parent,i)}.${l}`);let h=document.createElement("li");h.classList.value=u.classes;let p=document.createElement("a");p.href=n.base+u.url,p.classList.add("tsd-kind-icon"),p.innerHTML=l,h.append(p),e.appendChild(h)}}function fe(t,e){let r=t.querySelector(".current");if(!r)r=t.querySelector(e==1?"li:first-child":"li:last-child"),r&&r.classList.add("current");else{let n=r;if(e===1)do n=n.nextElementSibling;while(n instanceof HTMLElement&&n.offsetParent==null);else do n=n.previousElementSibling;while(n instanceof HTMLElement&&n.offsetParent==null);n&&(r.classList.remove("current"),n.classList.add("current"))}}function Ne(t,e){let r=t.querySelector(".current");if(r||(r=t.querySelector("li:first-child")),r){let n=r.querySelector("a");n&&(window.location.href=n.href),e.blur()}}function pe(t,e){if(e==="")return t;let r=t.toLocaleLowerCase(),n=e.toLocaleLowerCase(),i=[],s=0,o=r.indexOf(n);for(;o!=-1;)i.push(re(t.substring(s,o)),`${re(t.substring(o,o+n.length))}`),s=o+n.length,o=r.indexOf(n,s);return i.push(re(t.substring(s))),i.join("")}var je={"&":"&","<":"<",">":">","'":"'",'"':"""};function re(t){return t.replace(/[&<>"'"]/g,e=>je[e])}var ge=class{constructor(e,r){this.signature=e,this.description=r}addClass(e){return this.signature.classList.add(e),this.description.classList.add(e),this}removeClass(e){return this.signature.classList.remove(e),this.description.classList.remove(e),this}},ne=class extends Q{constructor(e){super(e);this.groups=[];this.index=-1;this.createGroups(),this.container&&(this.el.classList.add("active"),Array.from(this.el.children).forEach(r=>{r.addEventListener("touchstart",n=>this.onClick(n)),r.addEventListener("click",n=>this.onClick(n))}),this.container.classList.add("active"),this.setIndex(0))}setIndex(e){if(e<0&&(e=0),e>this.groups.length-1&&(e=this.groups.length-1),this.index==e)return;let r=this.groups[e];if(this.index>-1){let n=this.groups[this.index];n.removeClass("current").addClass("fade-out"),r.addClass("current"),r.addClass("fade-in"),I.instance.triggerResize(),setTimeout(()=>{n.removeClass("fade-out"),r.removeClass("fade-in")},300)}else r.addClass("current"),I.instance.triggerResize();this.index=e}createGroups(){let e=this.el.children;if(e.length<2)return;this.container=this.el.nextElementSibling;let r=this.container.children;this.groups=[];for(let n=0;n{r.signature===e.currentTarget&&this.setIndex(n)})}};var C="mousedown",xe="mousemove",_="mouseup",G={x:0,y:0},ye=!1,ie=!1,Be=!1,A=!1,Le=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);document.documentElement.classList.add(Le?"is-mobile":"not-mobile");Le&&"ontouchstart"in document.documentElement&&(Be=!0,C="touchstart",xe="touchmove",_="touchend");document.addEventListener(C,t=>{ie=!0,A=!1;let e=C=="touchstart"?t.targetTouches[0]:t;G.y=e.pageY||0,G.x=e.pageX||0});document.addEventListener(xe,t=>{if(!!ie&&!A){let e=C=="touchstart"?t.targetTouches[0]:t,r=G.x-(e.pageX||0),n=G.y-(e.pageY||0);A=Math.sqrt(r*r+n*n)>10}});document.addEventListener(_,()=>{ie=!1});document.addEventListener("click",t=>{ye&&(t.preventDefault(),t.stopImmediatePropagation(),ye=!1)});var se=class extends Q{constructor(e){super(e);this.className=this.el.dataset.toggle||"",this.el.addEventListener(_,r=>this.onPointerUp(r)),this.el.addEventListener("click",r=>r.preventDefault()),document.addEventListener(C,r=>this.onDocumentPointerDown(r)),document.addEventListener(_,r=>this.onDocumentPointerUp(r))}setActive(e){if(this.active==e)return;this.active=e,document.documentElement.classList.toggle("has-"+this.className,e),this.el.classList.toggle("active",e);let r=(this.active?"to-has-":"from-has-")+this.className;document.documentElement.classList.add(r),setTimeout(()=>document.documentElement.classList.remove(r),500)}onPointerUp(e){A||(this.setActive(!0),e.preventDefault())}onDocumentPointerDown(e){if(this.active){if(e.target.closest(".col-menu, .tsd-filter-group"))return;this.setActive(!1)}}onDocumentPointerUp(e){if(!A&&this.active&&e.target.closest(".col-menu")){let r=e.target.closest("a");if(r){let n=window.location.href;n.indexOf("#")!=-1&&(n=n.substr(0,n.indexOf("#"))),r.href.substr(0,n.length)==n&&setTimeout(()=>this.setActive(!1),250)}}}};var ae=class{constructor(e,r){this.key=e,this.value=r,this.defaultValue=r,this.initialize(),window.localStorage[this.key]&&this.setValue(this.fromLocalStorage(window.localStorage[this.key]))}initialize(){}setValue(e){if(this.value==e)return;let r=this.value;this.value=e,window.localStorage[this.key]=this.toLocalStorage(e),this.handleValueChange(r,e)}},oe=class extends ae{initialize(){let e=document.querySelector("#tsd-filter-"+this.key);!e||(this.checkbox=e,this.checkbox.addEventListener("change",()=>{this.setValue(this.checkbox.checked)}))}handleValueChange(e,r){!this.checkbox||(this.checkbox.checked=this.value,document.documentElement.classList.toggle("toggle-"+this.key,this.value!=this.defaultValue))}fromLocalStorage(e){return e=="true"}toLocalStorage(e){return e?"true":"false"}},Ee=class extends ae{initialize(){document.documentElement.classList.add("toggle-"+this.key+this.value);let e=document.querySelector("#tsd-filter-"+this.key);if(!e)return;this.select=e;let r=()=>{this.select.classList.add("active")},n=()=>{this.select.classList.remove("active")};this.select.addEventListener(C,r),this.select.addEventListener("mouseover",r),this.select.addEventListener("mouseleave",n),this.select.querySelectorAll("li").forEach(i=>{i.addEventListener(_,s=>{e.classList.remove("active"),this.setValue(s.target.dataset.value||"")})}),document.addEventListener(C,i=>{this.select.contains(i.target)||this.select.classList.remove("active")})}handleValueChange(e,r){this.select.querySelectorAll("li.selected").forEach(s=>{s.classList.remove("selected")});let n=this.select.querySelector('li[data-value="'+r+'"]'),i=this.select.querySelector(".tsd-select-label");n&&i&&(n.classList.add("selected"),i.textContent=n.textContent),document.documentElement.classList.remove("toggle-"+e),document.documentElement.classList.add("toggle-"+r)}fromLocalStorage(e){return e}toLocalStorage(e){return e}},Y=class extends Q{constructor(e){super(e);this.optionVisibility=new Ee("visibility","private"),this.optionInherited=new oe("inherited",!0),this.optionExternals=new oe("externals",!0)}static isSupported(){try{return typeof window.localStorage!="undefined"}catch{return!1}}};function we(t){let e=localStorage.getItem("tsd-theme")||"os";t.value=e,be(e),t.addEventListener("change",()=>{localStorage.setItem("tsd-theme",t.value),be(t.value)})}function be(t){switch(t){case"os":document.body.classList.remove("light","dark");break;case"light":document.body.classList.remove("dark"),document.body.classList.add("light");break;case"dark":document.body.classList.remove("light"),document.body.classList.add("dark");break}}ve();N(te,".menu-highlight");N(ne,".tsd-signatures");N(se,"a[data-toggle]");Y.isSupported()?N(Y,"#tsd-filter"):document.documentElement.classList.add("no-filter");var Te=document.getElementById("theme");Te&&we(Te);var qe=new X;Object.defineProperty(window,"app",{value:qe});})(); +/*! + * lunr.Builder + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.Index + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.Pipeline + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.Set + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.TokenSet + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.Vector + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.stemmer + * Copyright (C) 2020 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ +/*! + * lunr.stopWordFilter + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.tokenizer + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.trimmer + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.utils + * Copyright (C) 2020 Oliver Nightingale + */ +/** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 + * Copyright (C) 2020 Oliver Nightingale + * @license MIT + */ diff --git a/frontend/docs/assets/search.js b/frontend/docs/assets/search.js new file mode 100644 index 000000000..2b931b200 --- /dev/null +++ b/frontend/docs/assets/search.js @@ -0,0 +1 @@ +window.searchData = JSON.parse("{\"kinds\":{\"4\":\"Namespace\",\"8\":\"Enumeration\",\"16\":\"Enumeration member\",\"64\":\"Function\",\"256\":\"Interface\",\"1024\":\"Property\",\"2048\":\"Method\"},\"rows\":[{\"id\":0,\"kind\":4,\"name\":\"Components\",\"url\":\"modules/Components.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":1,\"kind\":64,\"name\":\"AdminRoute\",\"url\":\"modules/Components.html#AdminRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":2,\"kind\":64,\"name\":\"Footer\",\"url\":\"modules/Components.html#Footer\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":3,\"kind\":4,\"name\":\"Login\",\"url\":\"modules/Components.Login.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":4,\"kind\":64,\"name\":\"WelcomeText\",\"url\":\"modules/Components.Login.html#WelcomeText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Login\"},{\"id\":5,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/Components.Login.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Login\"},{\"id\":6,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/Components.Login.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Login\"},{\"id\":7,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/Components.Login.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Login\"},{\"id\":8,\"kind\":64,\"name\":\"NavBar\",\"url\":\"modules/Components.html#NavBar\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":9,\"kind\":64,\"name\":\"OSOCLetters\",\"url\":\"modules/Components.html#OSOCLetters\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":10,\"kind\":64,\"name\":\"PrivateRoute\",\"url\":\"modules/Components.html#PrivateRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":11,\"kind\":4,\"name\":\"Register\",\"url\":\"modules/Components.Register.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":12,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/Components.Register.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":13,\"kind\":64,\"name\":\"Name\",\"url\":\"modules/Components.Register.html#Name\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":14,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/Components.Register.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":15,\"kind\":64,\"name\":\"ConfirmPassword\",\"url\":\"modules/Components.Register.html#ConfirmPassword\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":16,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/Components.Register.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":17,\"kind\":64,\"name\":\"InfoText\",\"url\":\"modules/Components.Register.html#InfoText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":18,\"kind\":64,\"name\":\"BadInviteLink\",\"url\":\"modules/Components.Register.html#BadInviteLink\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":19,\"kind\":4,\"name\":\"Contexts\",\"url\":\"modules/Contexts.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":20,\"kind\":256,\"name\":\"AuthContextState\",\"url\":\"interfaces/Contexts.AuthContextState.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"Contexts\"},{\"id\":21,\"kind\":1024,\"name\":\"isLoggedIn\",\"url\":\"interfaces/Contexts.AuthContextState.html#isLoggedIn\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":22,\"kind\":2048,\"name\":\"setIsLoggedIn\",\"url\":\"interfaces/Contexts.AuthContextState.html#setIsLoggedIn\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":23,\"kind\":1024,\"name\":\"role\",\"url\":\"interfaces/Contexts.AuthContextState.html#role\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":24,\"kind\":2048,\"name\":\"setRole\",\"url\":\"interfaces/Contexts.AuthContextState.html#setRole\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":25,\"kind\":1024,\"name\":\"token\",\"url\":\"interfaces/Contexts.AuthContextState.html#token\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":26,\"kind\":2048,\"name\":\"setToken\",\"url\":\"interfaces/Contexts.AuthContextState.html#setToken\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":27,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/Contexts.AuthContextState.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":28,\"kind\":2048,\"name\":\"setEditions\",\"url\":\"interfaces/Contexts.AuthContextState.html#setEditions\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":29,\"kind\":64,\"name\":\"AuthProvider\",\"url\":\"modules/Contexts.html#AuthProvider\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Contexts\"},{\"id\":30,\"kind\":4,\"name\":\"Data\",\"url\":\"modules/Data.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":31,\"kind\":4,\"name\":\"Enums\",\"url\":\"modules/Data.Enums.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Data\"},{\"id\":32,\"kind\":8,\"name\":\"StorageKey\",\"url\":\"enums/Data.Enums.StorageKey.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"Data.Enums\"},{\"id\":33,\"kind\":16,\"name\":\"BEARER_TOKEN\",\"url\":\"enums/Data.Enums.StorageKey.html#BEARER_TOKEN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"Data.Enums.StorageKey\"},{\"id\":34,\"kind\":8,\"name\":\"Role\",\"url\":\"enums/Data.Enums.Role.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"Data.Enums\"},{\"id\":35,\"kind\":16,\"name\":\"ADMIN\",\"url\":\"enums/Data.Enums.Role.html#ADMIN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"Data.Enums.Role\"},{\"id\":36,\"kind\":16,\"name\":\"COACH\",\"url\":\"enums/Data.Enums.Role.html#COACH\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"Data.Enums.Role\"},{\"id\":37,\"kind\":4,\"name\":\"Interfaces\",\"url\":\"modules/Data.Interfaces.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Data\"},{\"id\":38,\"kind\":256,\"name\":\"User\",\"url\":\"interfaces/Data.Interfaces.User.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"Data.Interfaces\"},{\"id\":39,\"kind\":1024,\"name\":\"userId\",\"url\":\"interfaces/Data.Interfaces.User.html#userId\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Data.Interfaces.User\"},{\"id\":40,\"kind\":1024,\"name\":\"name\",\"url\":\"interfaces/Data.Interfaces.User.html#name\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Data.Interfaces.User\"},{\"id\":41,\"kind\":1024,\"name\":\"admin\",\"url\":\"interfaces/Data.Interfaces.User.html#admin\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Data.Interfaces.User\"},{\"id\":42,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/Data.Interfaces.User.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Data.Interfaces.User\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"parent\"],\"fieldVectors\":[[\"name/0\",[0,16.441]],[\"parent/0\",[]],[\"name/1\",[1,33.787]],[\"parent/1\",[0,1.595]],[\"name/2\",[2,33.787]],[\"parent/2\",[0,1.595]],[\"name/3\",[3,33.787]],[\"parent/3\",[0,1.595]],[\"name/4\",[4,33.787]],[\"parent/4\",[5,2.212]],[\"name/5\",[6,28.679]],[\"parent/5\",[5,2.212]],[\"name/6\",[7,28.679]],[\"parent/6\",[5,2.212]],[\"name/7\",[8,28.679]],[\"parent/7\",[5,2.212]],[\"name/8\",[9,33.787]],[\"parent/8\",[0,1.595]],[\"name/9\",[10,33.787]],[\"parent/9\",[0,1.595]],[\"name/10\",[11,33.787]],[\"parent/10\",[0,1.595]],[\"name/11\",[12,33.787]],[\"parent/11\",[0,1.595]],[\"name/12\",[8,28.679]],[\"parent/12\",[13,1.717]],[\"name/13\",[14,28.679]],[\"parent/13\",[13,1.717]],[\"name/14\",[7,28.679]],[\"parent/14\",[13,1.717]],[\"name/15\",[15,33.787]],[\"parent/15\",[13,1.717]],[\"name/16\",[6,28.679]],[\"parent/16\",[13,1.717]],[\"name/17\",[16,33.787]],[\"parent/17\",[13,1.717]],[\"name/18\",[17,33.787]],[\"parent/18\",[13,1.717]],[\"name/19\",[18,25.314]],[\"parent/19\",[]],[\"name/20\",[19,33.787]],[\"parent/20\",[18,2.456]],[\"name/21\",[20,33.787]],[\"parent/21\",[21,1.595]],[\"name/22\",[22,33.787]],[\"parent/22\",[21,1.595]],[\"name/23\",[23,28.679]],[\"parent/23\",[21,1.595]],[\"name/24\",[24,33.787]],[\"parent/24\",[21,1.595]],[\"name/25\",[25,33.787]],[\"parent/25\",[21,1.595]],[\"name/26\",[26,33.787]],[\"parent/26\",[21,1.595]],[\"name/27\",[27,28.679]],[\"parent/27\",[21,1.595]],[\"name/28\",[28,33.787]],[\"parent/28\",[21,1.595]],[\"name/29\",[29,33.787]],[\"parent/29\",[18,2.456]],[\"name/30\",[30,25.314]],[\"parent/30\",[]],[\"name/31\",[31,33.787]],[\"parent/31\",[30,2.456]],[\"name/32\",[32,33.787]],[\"parent/32\",[33,2.783]],[\"name/33\",[34,33.787]],[\"parent/33\",[35,3.278]],[\"name/34\",[23,28.679]],[\"parent/34\",[33,2.783]],[\"name/35\",[36,28.679]],[\"parent/35\",[37,2.783]],[\"name/36\",[38,33.787]],[\"parent/36\",[37,2.783]],[\"name/37\",[39,33.787]],[\"parent/37\",[30,2.456]],[\"name/38\",[40,33.787]],[\"parent/38\",[41,3.278]],[\"name/39\",[42,33.787]],[\"parent/39\",[43,2.212]],[\"name/40\",[14,28.679]],[\"parent/40\",[43,2.212]],[\"name/41\",[36,28.679]],[\"parent/41\",[43,2.212]],[\"name/42\",[27,28.679]],[\"parent/42\",[43,2.212]]],\"invertedIndex\":[[\"admin\",{\"_index\":36,\"name\":{\"35\":{},\"41\":{}},\"parent\":{}}],[\"adminroute\",{\"_index\":1,\"name\":{\"1\":{}},\"parent\":{}}],[\"authcontextstate\",{\"_index\":19,\"name\":{\"20\":{}},\"parent\":{}}],[\"authprovider\",{\"_index\":29,\"name\":{\"29\":{}},\"parent\":{}}],[\"badinvitelink\",{\"_index\":17,\"name\":{\"18\":{}},\"parent\":{}}],[\"bearer_token\",{\"_index\":34,\"name\":{\"33\":{}},\"parent\":{}}],[\"coach\",{\"_index\":38,\"name\":{\"36\":{}},\"parent\":{}}],[\"components\",{\"_index\":0,\"name\":{\"0\":{}},\"parent\":{\"1\":{},\"2\":{},\"3\":{},\"8\":{},\"9\":{},\"10\":{},\"11\":{}}}],[\"components.login\",{\"_index\":5,\"name\":{},\"parent\":{\"4\":{},\"5\":{},\"6\":{},\"7\":{}}}],[\"components.register\",{\"_index\":13,\"name\":{},\"parent\":{\"12\":{},\"13\":{},\"14\":{},\"15\":{},\"16\":{},\"17\":{},\"18\":{}}}],[\"confirmpassword\",{\"_index\":15,\"name\":{\"15\":{}},\"parent\":{}}],[\"contexts\",{\"_index\":18,\"name\":{\"19\":{}},\"parent\":{\"20\":{},\"29\":{}}}],[\"contexts.authcontextstate\",{\"_index\":21,\"name\":{},\"parent\":{\"21\":{},\"22\":{},\"23\":{},\"24\":{},\"25\":{},\"26\":{},\"27\":{},\"28\":{}}}],[\"data\",{\"_index\":30,\"name\":{\"30\":{}},\"parent\":{\"31\":{},\"37\":{}}}],[\"data.enums\",{\"_index\":33,\"name\":{},\"parent\":{\"32\":{},\"34\":{}}}],[\"data.enums.role\",{\"_index\":37,\"name\":{},\"parent\":{\"35\":{},\"36\":{}}}],[\"data.enums.storagekey\",{\"_index\":35,\"name\":{},\"parent\":{\"33\":{}}}],[\"data.interfaces\",{\"_index\":41,\"name\":{},\"parent\":{\"38\":{}}}],[\"data.interfaces.user\",{\"_index\":43,\"name\":{},\"parent\":{\"39\":{},\"40\":{},\"41\":{},\"42\":{}}}],[\"editions\",{\"_index\":27,\"name\":{\"27\":{},\"42\":{}},\"parent\":{}}],[\"email\",{\"_index\":8,\"name\":{\"7\":{},\"12\":{}},\"parent\":{}}],[\"enums\",{\"_index\":31,\"name\":{\"31\":{}},\"parent\":{}}],[\"footer\",{\"_index\":2,\"name\":{\"2\":{}},\"parent\":{}}],[\"infotext\",{\"_index\":16,\"name\":{\"17\":{}},\"parent\":{}}],[\"interfaces\",{\"_index\":39,\"name\":{\"37\":{}},\"parent\":{}}],[\"isloggedin\",{\"_index\":20,\"name\":{\"21\":{}},\"parent\":{}}],[\"login\",{\"_index\":3,\"name\":{\"3\":{}},\"parent\":{}}],[\"name\",{\"_index\":14,\"name\":{\"13\":{},\"40\":{}},\"parent\":{}}],[\"navbar\",{\"_index\":9,\"name\":{\"8\":{}},\"parent\":{}}],[\"osocletters\",{\"_index\":10,\"name\":{\"9\":{}},\"parent\":{}}],[\"password\",{\"_index\":7,\"name\":{\"6\":{},\"14\":{}},\"parent\":{}}],[\"privateroute\",{\"_index\":11,\"name\":{\"10\":{}},\"parent\":{}}],[\"register\",{\"_index\":12,\"name\":{\"11\":{}},\"parent\":{}}],[\"role\",{\"_index\":23,\"name\":{\"23\":{},\"34\":{}},\"parent\":{}}],[\"seteditions\",{\"_index\":28,\"name\":{\"28\":{}},\"parent\":{}}],[\"setisloggedin\",{\"_index\":22,\"name\":{\"22\":{}},\"parent\":{}}],[\"setrole\",{\"_index\":24,\"name\":{\"24\":{}},\"parent\":{}}],[\"settoken\",{\"_index\":26,\"name\":{\"26\":{}},\"parent\":{}}],[\"socialbuttons\",{\"_index\":6,\"name\":{\"5\":{},\"16\":{}},\"parent\":{}}],[\"storagekey\",{\"_index\":32,\"name\":{\"32\":{}},\"parent\":{}}],[\"token\",{\"_index\":25,\"name\":{\"25\":{}},\"parent\":{}}],[\"user\",{\"_index\":40,\"name\":{\"38\":{}},\"parent\":{}}],[\"userid\",{\"_index\":42,\"name\":{\"39\":{}},\"parent\":{}}],[\"welcometext\",{\"_index\":4,\"name\":{\"4\":{}},\"parent\":{}}]],\"pipeline\":[]}}"); \ No newline at end of file diff --git a/frontend/docs/assets/style.css b/frontend/docs/assets/style.css new file mode 100644 index 000000000..a16ed029e --- /dev/null +++ b/frontend/docs/assets/style.css @@ -0,0 +1,1413 @@ +@import url("./icons.css"); + +:root { + /* Light */ + --light-color-background: #fcfcfc; + --light-color-secondary-background: #fff; + --light-color-text: #222; + --light-color-text-aside: #707070; + --light-color-link: #4da6ff; + --light-color-menu-divider: #eee; + --light-color-menu-divider-focus: #000; + --light-color-menu-label: #707070; + --light-color-panel: var(--light-color-secondary-background); + --light-color-panel-divider: #eee; + --light-color-comment-tag: #707070; + --light-color-comment-tag-text: #fff; + --light-color-ts: #9600ff; + --light-color-ts-interface: #647f1b; + --light-color-ts-enum: #937210; + --light-color-ts-class: #0672de; + --light-color-ts-private: #707070; + --light-color-toolbar: #fff; + --light-color-toolbar-text: #333; + --light-icon-filter: invert(0); + --light-external-icon: url("data:image/svg+xml;utf8,"); + + /* Dark */ + --dark-color-background: #36393f; + --dark-color-secondary-background: #2f3136; + --dark-color-text: #ffffff; + --dark-color-text-aside: #e6e4e4; + --dark-color-link: #00aff4; + --dark-color-menu-divider: #eee; + --dark-color-menu-divider-focus: #000; + --dark-color-menu-label: #707070; + --dark-color-panel: var(--dark-color-secondary-background); + --dark-color-panel-divider: #818181; + --dark-color-comment-tag: #dcddde; + --dark-color-comment-tag-text: #2f3136; + --dark-color-ts: #c97dff; + --dark-color-ts-interface: #9cbe3c; + --dark-color-ts-enum: #d6ab29; + --dark-color-ts-class: #3695f3; + --dark-color-ts-private: #e2e2e2; + --dark-color-toolbar: #34373c; + --dark-color-toolbar-text: #ffffff; + --dark-icon-filter: invert(1); + --dark-external-icon: url("data:image/svg+xml;utf8,"); +} + +@media (prefers-color-scheme: light) { + :root { + --color-background: var(--light-color-background); + --color-secondary-background: var(--light-color-secondary-background); + --color-text: var(--light-color-text); + --color-text-aside: var(--light-color-text-aside); + --color-link: var(--light-color-link); + --color-menu-divider: var(--light-color-menu-divider); + --color-menu-divider-focus: var(--light-color-menu-divider-focus); + --color-menu-label: var(--light-color-menu-label); + --color-panel: var(--light-color-panel); + --color-panel-divider: var(--light-color-panel-divider); + --color-comment-tag: var(--light-color-comment-tag); + --color-comment-tag-text: var(--light-color-comment-tag-text); + --color-ts: var(--light-color-ts); + --color-ts-interface: var(--light-color-ts-interface); + --color-ts-enum: var(--light-color-ts-enum); + --color-ts-class: var(--light-color-ts-class); + --color-ts-private: var(--light-color-ts-private); + --color-toolbar: var(--light-color-toolbar); + --color-toolbar-text: var(--light-color-toolbar-text); + --icon-filter: var(--light-icon-filter); + --external-icon: var(--light-external-icon); + } +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background: var(--dark-color-background); + --color-secondary-background: var(--dark-color-secondary-background); + --color-text: var(--dark-color-text); + --color-text-aside: var(--dark-color-text-aside); + --color-link: var(--dark-color-link); + --color-menu-divider: var(--dark-color-menu-divider); + --color-menu-divider-focus: var(--dark-color-menu-divider-focus); + --color-menu-label: var(--dark-color-menu-label); + --color-panel: var(--dark-color-panel); + --color-panel-divider: var(--dark-color-panel-divider); + --color-comment-tag: var(--dark-color-comment-tag); + --color-comment-tag-text: var(--dark-color-comment-tag-text); + --color-ts: var(--dark-color-ts); + --color-ts-interface: var(--dark-color-ts-interface); + --color-ts-enum: var(--dark-color-ts-enum); + --color-ts-class: var(--dark-color-ts-class); + --color-ts-private: var(--dark-color-ts-private); + --color-toolbar: var(--dark-color-toolbar); + --color-toolbar-text: var(--dark-color-toolbar-text); + --icon-filter: var(--dark-icon-filter); + --external-icon: var(--dark-external-icon); + } +} + +body { + margin: 0; +} + +body.light { + --color-background: var(--light-color-background); + --color-secondary-background: var(--light-color-secondary-background); + --color-text: var(--light-color-text); + --color-text-aside: var(--light-color-text-aside); + --color-link: var(--light-color-link); + --color-menu-divider: var(--light-color-menu-divider); + --color-menu-divider-focus: var(--light-color-menu-divider-focus); + --color-menu-label: var(--light-color-menu-label); + --color-panel: var(--light-color-panel); + --color-panel-divider: var(--light-color-panel-divider); + --color-comment-tag: var(--light-color-comment-tag); + --color-comment-tag-text: var(--light-color-comment-tag-text); + --color-ts: var(--light-color-ts); + --color-ts-interface: var(--light-color-ts-interface); + --color-ts-enum: var(--light-color-ts-enum); + --color-ts-class: var(--light-color-ts-class); + --color-ts-private: var(--light-color-ts-private); + --color-toolbar: var(--light-color-toolbar); + --color-toolbar-text: var(--light-color-toolbar-text); + --icon-filter: var(--light-icon-filter); + --external-icon: var(--light-external-icon); +} + +body.dark { + --color-background: var(--dark-color-background); + --color-secondary-background: var(--dark-color-secondary-background); + --color-text: var(--dark-color-text); + --color-text-aside: var(--dark-color-text-aside); + --color-link: var(--dark-color-link); + --color-menu-divider: var(--dark-color-menu-divider); + --color-menu-divider-focus: var(--dark-color-menu-divider-focus); + --color-menu-label: var(--dark-color-menu-label); + --color-panel: var(--dark-color-panel); + --color-panel-divider: var(--dark-color-panel-divider); + --color-comment-tag: var(--dark-color-comment-tag); + --color-comment-tag-text: var(--dark-color-comment-tag-text); + --color-ts: var(--dark-color-ts); + --color-ts-interface: var(--dark-color-ts-interface); + --color-ts-enum: var(--dark-color-ts-enum); + --color-ts-class: var(--dark-color-ts-class); + --color-ts-private: var(--dark-color-ts-private); + --color-toolbar: var(--dark-color-toolbar); + --color-toolbar-text: var(--dark-color-toolbar-text); + --icon-filter: var(--dark-icon-filter); + --external-icon: var(--dark-external-icon); +} + +h1, +h2, +h3, +h4, +h5, +h6 { + line-height: 1.2; +} + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +h2 { + font-size: 1.5em; + margin: 0.83em 0; +} + +h3 { + font-size: 1.17em; + margin: 1em 0; +} + +h4, +.tsd-index-panel h3 { + font-size: 1em; + margin: 1.33em 0; +} + +h5 { + font-size: 0.83em; + margin: 1.67em 0; +} + +h6 { + font-size: 0.67em; + margin: 2.33em 0; +} + +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +dl, +menu, +ol, +ul { + margin: 1em 0; +} + +dd { + margin: 0 0 0 40px; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 40px; +} +@media (max-width: 640px) { + .container { + padding: 0 20px; + } +} + +.container-main { + padding-bottom: 200px; +} + +.row { + display: flex; + position: relative; + margin: 0 -10px; +} +.row:after { + visibility: hidden; + display: block; + content: ""; + clear: both; + height: 0; +} + +.col-4, +.col-8 { + box-sizing: border-box; + float: left; + padding: 0 10px; +} + +.col-4 { + width: 33.3333333333%; +} +.col-8 { + width: 66.6666666667%; +} + +ul.tsd-descriptions > li > :first-child, +.tsd-panel > :first-child, +.col-8 > :first-child, +.col-4 > :first-child, +ul.tsd-descriptions > li > :first-child > :first-child, +.tsd-panel > :first-child > :first-child, +.col-8 > :first-child > :first-child, +.col-4 > :first-child > :first-child, +ul.tsd-descriptions > li > :first-child > :first-child > :first-child, +.tsd-panel > :first-child > :first-child > :first-child, +.col-8 > :first-child > :first-child > :first-child, +.col-4 > :first-child > :first-child > :first-child { + margin-top: 0; +} +ul.tsd-descriptions > li > :last-child, +.tsd-panel > :last-child, +.col-8 > :last-child, +.col-4 > :last-child, +ul.tsd-descriptions > li > :last-child > :last-child, +.tsd-panel > :last-child > :last-child, +.col-8 > :last-child > :last-child, +.col-4 > :last-child > :last-child, +ul.tsd-descriptions > li > :last-child > :last-child > :last-child, +.tsd-panel > :last-child > :last-child > :last-child, +.col-8 > :last-child > :last-child > :last-child, +.col-4 > :last-child > :last-child > :last-child { + margin-bottom: 0; +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes fade-out { + from { + opacity: 1; + visibility: visible; + } + to { + opacity: 0; + } +} +@keyframes fade-in-delayed { + 0% { + opacity: 0; + } + 33% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +@keyframes fade-out-delayed { + 0% { + opacity: 1; + visibility: visible; + } + 66% { + opacity: 0; + } + 100% { + opacity: 0; + } +} +@keyframes shift-to-left { + from { + transform: translate(0, 0); + } + to { + transform: translate(-25%, 0); + } +} +@keyframes unshift-to-left { + from { + transform: translate(-25%, 0); + } + to { + transform: translate(0, 0); + } +} +@keyframes pop-in-from-right { + from { + transform: translate(100%, 0); + } + to { + transform: translate(0, 0); + } +} +@keyframes pop-out-to-right { + from { + transform: translate(0, 0); + visibility: visible; + } + to { + transform: translate(100%, 0); + } +} +body { + background: var(--color-background); + font-family: "Segoe UI", sans-serif; + font-size: 16px; + color: var(--color-text); +} + +a { + color: var(--color-link); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +a.external[target="_blank"] { + background-image: var(--external-icon); + background-position: top 3px right; + background-repeat: no-repeat; + padding-right: 13px; +} + +code, +pre { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + padding: 0.2em; + margin: 0; + font-size: 14px; +} + +pre { + padding: 10px; +} +pre code { + padding: 0; + font-size: 100%; +} + +blockquote { + margin: 1em 0; + padding-left: 1em; + border-left: 4px solid gray; +} + +.tsd-typography { + line-height: 1.333em; +} +.tsd-typography ul { + list-style: square; + padding: 0 0 0 20px; + margin: 0; +} +.tsd-typography h4, +.tsd-typography .tsd-index-panel h3, +.tsd-index-panel .tsd-typography h3, +.tsd-typography h5, +.tsd-typography h6 { + font-size: 1em; + margin: 0; +} +.tsd-typography h5, +.tsd-typography h6 { + font-weight: normal; +} +.tsd-typography p, +.tsd-typography ul, +.tsd-typography ol { + margin: 1em 0; +} + +@media (min-width: 901px) and (max-width: 1024px) { + html .col-content { + width: 72%; + } + html .col-menu { + width: 28%; + } + html .tsd-navigation { + padding-left: 10px; + } +} +@media (max-width: 900px) { + html .col-content { + float: none; + width: 100%; + } + html .col-menu { + position: fixed !important; + overflow: auto; + -webkit-overflow-scrolling: touch; + z-index: 1024; + top: 0 !important; + bottom: 0 !important; + left: auto !important; + right: 0 !important; + width: 100%; + padding: 20px 20px 0 0; + max-width: 450px; + visibility: hidden; + background-color: var(--color-panel); + transform: translate(100%, 0); + } + html .col-menu > *:last-child { + padding-bottom: 20px; + } + html .overlay { + content: ""; + display: block; + position: fixed; + z-index: 1023; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.75); + visibility: hidden; + } + + .to-has-menu .overlay { + animation: fade-in 0.4s; + } + + .to-has-menu :is(header, footer, .col-content) { + animation: shift-to-left 0.4s; + } + + .to-has-menu .col-menu { + animation: pop-in-from-right 0.4s; + } + + .from-has-menu .overlay { + animation: fade-out 0.4s; + } + + .from-has-menu :is(header, footer, .col-content) { + animation: unshift-to-left 0.4s; + } + + .from-has-menu .col-menu { + animation: pop-out-to-right 0.4s; + } + + .has-menu body { + overflow: hidden; + } + .has-menu .overlay { + visibility: visible; + } + .has-menu :is(header, footer, .col-content) { + transform: translate(-25%, 0); + } + .has-menu .col-menu { + visibility: visible; + transform: translate(0, 0); + display: grid; + grid-template-rows: auto 1fr; + max-height: 100vh; + } + .has-menu .tsd-navigation { + max-height: 100%; + } +} + +.tsd-page-title { + padding: 70px 0 20px 0; + margin: 0 0 40px 0; + background: var(--color-panel); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.35); +} +.tsd-page-title h1 { + margin: 0; +} + +.tsd-breadcrumb { + margin: 0; + padding: 0; + color: var(--color-text-aside); +} +.tsd-breadcrumb a { + color: var(--color-text-aside); + text-decoration: none; +} +.tsd-breadcrumb a:hover { + text-decoration: underline; +} +.tsd-breadcrumb li { + display: inline; +} +.tsd-breadcrumb li:after { + content: " / "; +} + +dl.tsd-comment-tags { + overflow: hidden; +} +dl.tsd-comment-tags dt { + float: left; + padding: 1px 5px; + margin: 0 10px 0 0; + border-radius: 4px; + border: 1px solid var(--color-comment-tag); + color: var(--color-comment-tag); + font-size: 0.8em; + font-weight: normal; +} +dl.tsd-comment-tags dd { + margin: 0 0 10px 0; +} +dl.tsd-comment-tags dd:before, +dl.tsd-comment-tags dd:after { + display: table; + content: " "; +} +dl.tsd-comment-tags dd pre, +dl.tsd-comment-tags dd:after { + clear: both; +} +dl.tsd-comment-tags p { + margin: 0; +} + +.tsd-panel.tsd-comment .lead { + font-size: 1.1em; + line-height: 1.333em; + margin-bottom: 2em; +} +.tsd-panel.tsd-comment .lead:last-child { + margin-bottom: 0; +} + +.toggle-protected .tsd-is-private { + display: none; +} + +.toggle-public .tsd-is-private, +.toggle-public .tsd-is-protected, +.toggle-public .tsd-is-private-protected { + display: none; +} + +.toggle-inherited .tsd-is-inherited { + display: none; +} + +.toggle-externals .tsd-is-external { + display: none; +} + +#tsd-filter { + position: relative; + display: inline-block; + height: 40px; + vertical-align: bottom; +} +.no-filter #tsd-filter { + display: none; +} +#tsd-filter .tsd-filter-group { + display: inline-block; + height: 40px; + vertical-align: bottom; + white-space: nowrap; +} +#tsd-filter input { + display: none; +} +@media (max-width: 900px) { + #tsd-filter .tsd-filter-group { + display: block; + position: absolute; + top: 40px; + right: 20px; + height: auto; + background-color: var(--color-panel); + visibility: hidden; + transform: translate(50%, 0); + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); + } + .has-options #tsd-filter .tsd-filter-group { + visibility: visible; + } + .to-has-options #tsd-filter .tsd-filter-group { + animation: fade-in 0.2s; + } + .from-has-options #tsd-filter .tsd-filter-group { + animation: fade-out 0.2s; + } + #tsd-filter label, + #tsd-filter .tsd-select { + display: block; + padding-right: 20px; + } +} + +footer { + border-top: 1px solid var(--color-panel-divider); + background-color: var(--color-panel); +} +footer:after { + content: ""; + display: table; +} +footer.with-border-bottom { + border-bottom: 1px solid var(--color-panel-divider); +} +footer .tsd-legend-group { + font-size: 0; +} +footer .tsd-legend { + display: inline-block; + width: 25%; + padding: 0; + font-size: 16px; + list-style: none; + line-height: 1.333em; + vertical-align: top; +} +@media (max-width: 900px) { + footer .tsd-legend { + width: 50%; + } +} + +.tsd-hierarchy { + list-style: square; + padding: 0 0 0 20px; + margin: 0; +} +.tsd-hierarchy .target { + font-weight: bold; +} + +.tsd-index-panel .tsd-index-content { + margin-bottom: -30px !important; +} +.tsd-index-panel .tsd-index-section { + margin-bottom: 30px !important; +} +.tsd-index-panel h3 { + margin: 0 -20px 10px -20px; + padding: 0 20px 10px 20px; + border-bottom: 1px solid var(--color-panel-divider); +} +.tsd-index-panel ul.tsd-index-list { + -webkit-column-count: 3; + -moz-column-count: 3; + -ms-column-count: 3; + -o-column-count: 3; + column-count: 3; + -webkit-column-gap: 20px; + -moz-column-gap: 20px; + -ms-column-gap: 20px; + -o-column-gap: 20px; + column-gap: 20px; + padding: 0; + list-style: none; + line-height: 1.333em; +} +@media (max-width: 900px) { + .tsd-index-panel ul.tsd-index-list { + -webkit-column-count: 1; + -moz-column-count: 1; + -ms-column-count: 1; + -o-column-count: 1; + column-count: 1; + } +} +@media (min-width: 901px) and (max-width: 1024px) { + .tsd-index-panel ul.tsd-index-list { + -webkit-column-count: 2; + -moz-column-count: 2; + -ms-column-count: 2; + -o-column-count: 2; + column-count: 2; + } +} +.tsd-index-panel ul.tsd-index-list li { + -webkit-page-break-inside: avoid; + -moz-page-break-inside: avoid; + -ms-page-break-inside: avoid; + -o-page-break-inside: avoid; + page-break-inside: avoid; +} +.tsd-index-panel a, +.tsd-index-panel .tsd-parent-kind-module a { + color: var(--color-ts); +} +.tsd-index-panel .tsd-parent-kind-interface a { + color: var(--color-ts-interface); +} +.tsd-index-panel .tsd-parent-kind-enum a { + color: var(--color-ts-enum); +} +.tsd-index-panel .tsd-parent-kind-class a { + color: var(--color-ts-class); +} +.tsd-index-panel .tsd-kind-module a { + color: var(--color-ts); +} +.tsd-index-panel .tsd-kind-interface a { + color: var(--color-ts-interface); +} +.tsd-index-panel .tsd-kind-enum a { + color: var(--color-ts-enum); +} +.tsd-index-panel .tsd-kind-class a { + color: var(--color-ts-class); +} +.tsd-index-panel .tsd-is-private a { + color: var(--color-ts-private); +} + +.tsd-flag { + display: inline-block; + padding: 1px 5px; + border-radius: 4px; + color: var(--color-comment-tag-text); + background-color: var(--color-comment-tag); + text-indent: 0; + font-size: 14px; + font-weight: normal; +} + +.tsd-anchor { + position: absolute; + top: -100px; +} + +.tsd-member { + position: relative; +} +.tsd-member .tsd-anchor + h3 { + margin-top: 0; + margin-bottom: 0; + border-bottom: none; +} +.tsd-member [data-tsd-kind] { + color: var(--color-ts); +} +.tsd-member [data-tsd-kind="Interface"] { + color: var(--color-ts-interface); +} +.tsd-member [data-tsd-kind="Enum"] { + color: var(--color-ts-enum); +} +.tsd-member [data-tsd-kind="Class"] { + color: var(--color-ts-class); +} +.tsd-member [data-tsd-kind="Private"] { + color: var(--color-ts-private); +} + +.tsd-navigation { + margin: 0 0 0 40px; +} +.tsd-navigation a { + display: block; + padding-top: 2px; + padding-bottom: 2px; + border-left: 2px solid transparent; + color: var(--color-text); + text-decoration: none; + transition: border-left-color 0.1s; +} +.tsd-navigation a:hover { + text-decoration: underline; +} +.tsd-navigation ul { + margin: 0; + padding: 0; + list-style: none; +} +.tsd-navigation li { + padding: 0; +} + +.tsd-navigation.primary { + padding-bottom: 40px; +} +.tsd-navigation.primary a { + display: block; + padding-top: 6px; + padding-bottom: 6px; +} +.tsd-navigation.primary ul li a { + padding-left: 5px; +} +.tsd-navigation.primary ul li li a { + padding-left: 25px; +} +.tsd-navigation.primary ul li li li a { + padding-left: 45px; +} +.tsd-navigation.primary ul li li li li a { + padding-left: 65px; +} +.tsd-navigation.primary ul li li li li li a { + padding-left: 85px; +} +.tsd-navigation.primary ul li li li li li li a { + padding-left: 105px; +} +.tsd-navigation.primary > ul { + border-bottom: 1px solid var(--color-panel-divider); +} +.tsd-navigation.primary li { + border-top: 1px solid var(--color-panel-divider); +} +.tsd-navigation.primary li.current > a { + font-weight: bold; +} +.tsd-navigation.primary li.label span { + display: block; + padding: 20px 0 6px 5px; + color: var(--color-menu-label); +} +.tsd-navigation.primary li.globals + li > span, +.tsd-navigation.primary li.globals + li > a { + padding-top: 20px; +} + +.tsd-navigation.secondary { + max-height: calc(100vh - 1rem - 40px); + overflow: auto; + position: sticky; + top: calc(0.5rem + 40px); + transition: 0.3s; +} +.tsd-navigation.secondary.tsd-navigation--toolbar-hide { + max-height: calc(100vh - 1rem); + top: 0.5rem; +} +.tsd-navigation.secondary ul { + transition: opacity 0.2s; +} +.tsd-navigation.secondary ul li a { + padding-left: 25px; +} +.tsd-navigation.secondary ul li li a { + padding-left: 45px; +} +.tsd-navigation.secondary ul li li li a { + padding-left: 65px; +} +.tsd-navigation.secondary ul li li li li a { + padding-left: 85px; +} +.tsd-navigation.secondary ul li li li li li a { + padding-left: 105px; +} +.tsd-navigation.secondary ul li li li li li li a { + padding-left: 125px; +} +.tsd-navigation.secondary ul.current a { + border-left-color: var(--color-panel-divider); +} +.tsd-navigation.secondary li.focus > a, +.tsd-navigation.secondary ul.current li.focus > a { + border-left-color: var(--color-menu-divider-focus); +} +.tsd-navigation.secondary li.current { + margin-top: 20px; + margin-bottom: 20px; + border-left-color: var(--color-panel-divider); +} +.tsd-navigation.secondary li.current > a { + font-weight: bold; +} + +@media (min-width: 901px) { + .menu-sticky-wrap { + position: static; + } +} + +.tsd-panel { + margin: 20px 0; + padding: 20px; + background-color: var(--color-panel); + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); +} +.tsd-panel:empty { + display: none; +} +.tsd-panel > h1, +.tsd-panel > h2, +.tsd-panel > h3 { + margin: 1.5em -20px 10px -20px; + padding: 0 20px 10px 20px; + border-bottom: 1px solid var(--color-panel-divider); +} +.tsd-panel > h1.tsd-before-signature, +.tsd-panel > h2.tsd-before-signature, +.tsd-panel > h3.tsd-before-signature { + margin-bottom: 0; + border-bottom: 0; +} +.tsd-panel table { + display: block; + width: 100%; + overflow: auto; + margin-top: 10px; + word-break: normal; + word-break: keep-all; + border-collapse: collapse; +} +.tsd-panel table th { + font-weight: bold; +} +.tsd-panel table th, +.tsd-panel table td { + padding: 6px 13px; + border: 1px solid var(--color-panel-divider); +} +.tsd-panel table tr { + background: var(--color-background); +} +.tsd-panel table tr:nth-child(even) { + background: var(--color-secondary-background); +} + +.tsd-panel-group { + margin: 60px 0; +} +.tsd-panel-group > h1, +.tsd-panel-group > h2, +.tsd-panel-group > h3 { + padding-left: 20px; + padding-right: 20px; +} + +#tsd-search { + transition: background-color 0.2s; +} +#tsd-search .title { + position: relative; + z-index: 2; +} +#tsd-search .field { + position: absolute; + left: 0; + top: 0; + right: 40px; + height: 40px; +} +#tsd-search .field input { + box-sizing: border-box; + position: relative; + top: -50px; + z-index: 1; + width: 100%; + padding: 0 10px; + opacity: 0; + outline: 0; + border: 0; + background: transparent; + color: var(--color-text); +} +#tsd-search .field label { + position: absolute; + overflow: hidden; + right: -40px; +} +#tsd-search .field input, +#tsd-search .title { + transition: opacity 0.2s; +} +#tsd-search .results { + position: absolute; + visibility: hidden; + top: 40px; + width: 100%; + margin: 0; + padding: 0; + list-style: none; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); +} +#tsd-search .results li { + padding: 0 10px; + background-color: var(--color-background); +} +#tsd-search .results li:nth-child(even) { + background-color: var(--color-panel); +} +#tsd-search .results li.state { + display: none; +} +#tsd-search .results li.current, +#tsd-search .results li:hover { + background-color: var(--color-panel-divider); +} +#tsd-search .results a { + display: block; +} +#tsd-search .results a:before { + top: 10px; +} +#tsd-search .results span.parent { + color: var(--color-text-aside); + font-weight: normal; +} +#tsd-search.has-focus { + background-color: var(--color-panel-divider); +} +#tsd-search.has-focus .field input { + top: 0; + opacity: 1; +} +#tsd-search.has-focus .title { + z-index: 0; + opacity: 0; +} +#tsd-search.has-focus .results { + visibility: visible; +} +#tsd-search.loading .results li.state.loading { + display: block; +} +#tsd-search.failure .results li.state.failure { + display: block; +} + +.tsd-signature { + margin: 0 0 1em 0; + padding: 10px; + border: 1px solid var(--color-panel-divider); + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + font-size: 14px; + overflow-x: auto; +} +.tsd-signature.tsd-kind-icon { + padding-left: 30px; +} +.tsd-signature.tsd-kind-icon:before { + top: 10px; + left: 10px; +} +.tsd-panel > .tsd-signature { + margin-left: -20px; + margin-right: -20px; + border-width: 1px 0; +} +.tsd-panel > .tsd-signature.tsd-kind-icon { + padding-left: 40px; +} +.tsd-panel > .tsd-signature.tsd-kind-icon:before { + left: 20px; +} + +.tsd-signature-symbol { + color: var(--color-text-aside); + font-weight: normal; +} + +.tsd-signature-type { + font-style: italic; + font-weight: normal; +} + +.tsd-signatures { + padding: 0; + margin: 0 0 1em 0; + border: 1px solid var(--color-panel-divider); +} +.tsd-signatures .tsd-signature { + margin: 0; + border-width: 1px 0 0 0; + transition: background-color 0.1s; +} +.tsd-signatures .tsd-signature:first-child { + border-top-width: 0; +} +.tsd-signatures .tsd-signature.current { + background-color: var(--color-panel-divider); +} +.tsd-signatures.active > .tsd-signature { + cursor: pointer; +} +.tsd-panel > .tsd-signatures { + margin-left: -20px; + margin-right: -20px; + border-width: 1px 0; +} +.tsd-panel > .tsd-signatures .tsd-signature.tsd-kind-icon { + padding-left: 40px; +} +.tsd-panel > .tsd-signatures .tsd-signature.tsd-kind-icon:before { + left: 20px; +} +.tsd-panel > a.anchor + .tsd-signatures { + border-top-width: 0; + margin-top: -20px; +} + +ul.tsd-descriptions { + position: relative; + overflow: hidden; + padding: 0; + list-style: none; +} +ul.tsd-descriptions.active > .tsd-description { + display: none; +} +ul.tsd-descriptions.active > .tsd-description.current { + display: block; +} +ul.tsd-descriptions.active > .tsd-description.fade-in { + animation: fade-in-delayed 0.3s; +} +ul.tsd-descriptions.active > .tsd-description.fade-out { + animation: fade-out-delayed 0.3s; + position: absolute; + display: block; + top: 0; + left: 0; + right: 0; + opacity: 0; + visibility: hidden; +} +ul.tsd-descriptions h4, +ul.tsd-descriptions .tsd-index-panel h3, +.tsd-index-panel ul.tsd-descriptions h3 { + font-size: 16px; + margin: 1em 0 0.5em 0; +} + +ul.tsd-parameters, +ul.tsd-type-parameters { + list-style: square; + margin: 0; + padding-left: 20px; +} +ul.tsd-parameters > li.tsd-parameter-signature, +ul.tsd-type-parameters > li.tsd-parameter-signature { + list-style: none; + margin-left: -20px; +} +ul.tsd-parameters h5, +ul.tsd-type-parameters h5 { + font-size: 16px; + margin: 1em 0 0.5em 0; +} +ul.tsd-parameters .tsd-comment, +ul.tsd-type-parameters .tsd-comment { + margin-top: -0.5em; +} + +.tsd-sources { + font-size: 14px; + color: var(--color-text-aside); + margin: 0 0 1em 0; +} +.tsd-sources a { + color: var(--color-text-aside); + text-decoration: underline; +} +.tsd-sources ul, +.tsd-sources p { + margin: 0 !important; +} +.tsd-sources ul { + list-style: none; + padding: 0; +} + +.tsd-page-toolbar { + position: fixed; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 40px; + color: var(--color-toolbar-text); + background: var(--color-toolbar); + border-bottom: 1px solid var(--color-panel-divider); + transition: transform 0.3s linear; +} +.tsd-page-toolbar a { + color: var(--color-toolbar-text); + text-decoration: none; +} +.tsd-page-toolbar a.title { + font-weight: bold; +} +.tsd-page-toolbar a.title:hover { + text-decoration: underline; +} +.tsd-page-toolbar .table-wrap { + display: table; + width: 100%; + height: 40px; +} +.tsd-page-toolbar .table-cell { + display: table-cell; + position: relative; + white-space: nowrap; + line-height: 40px; +} +.tsd-page-toolbar .table-cell:first-child { + width: 100%; +} + +.tsd-page-toolbar--hide { + transform: translateY(-100%); +} + +.tsd-select .tsd-select-list li:before, +.tsd-select .tsd-select-label:before, +.tsd-widget:before { + content: ""; + display: inline-block; + width: 40px; + height: 40px; + margin: 0 -8px 0 0; + background-image: url(./widgets.png); + background-repeat: no-repeat; + text-indent: -1024px; + vertical-align: bottom; + filter: var(--icon-filter); +} +@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { + .tsd-select .tsd-select-list li:before, + .tsd-select .tsd-select-label:before, + .tsd-widget:before { + background-image: url(./widgets@2x.png); + background-size: 320px 40px; + } +} + +.tsd-widget { + display: inline-block; + overflow: hidden; + opacity: 0.8; + height: 40px; + transition: opacity 0.1s, background-color 0.2s; + vertical-align: bottom; + cursor: pointer; +} +.tsd-widget:hover { + opacity: 0.9; +} +.tsd-widget.active { + opacity: 1; + background-color: var(--color-panel-divider); +} +.tsd-widget.no-caption { + width: 40px; +} +.tsd-widget.no-caption:before { + margin: 0; +} +.tsd-widget.search:before { + background-position: 0 0; +} +.tsd-widget.menu:before { + background-position: -40px 0; +} +.tsd-widget.options:before { + background-position: -80px 0; +} +.tsd-widget.options, +.tsd-widget.menu { + display: none; +} +@media (max-width: 900px) { + .tsd-widget.options, + .tsd-widget.menu { + display: inline-block; + } +} +input[type="checkbox"] + .tsd-widget:before { + background-position: -120px 0; +} +input[type="checkbox"]:checked + .tsd-widget:before { + background-position: -160px 0; +} + +.tsd-select { + position: relative; + display: inline-block; + height: 40px; + transition: opacity 0.1s, background-color 0.2s; + vertical-align: bottom; + cursor: pointer; +} +.tsd-select .tsd-select-label { + opacity: 0.6; + transition: opacity 0.2s; +} +.tsd-select .tsd-select-label:before { + background-position: -240px 0; +} +.tsd-select.active .tsd-select-label { + opacity: 0.8; +} +.tsd-select.active .tsd-select-list { + visibility: visible; + opacity: 1; + transition-delay: 0s; +} +.tsd-select .tsd-select-list { + position: absolute; + visibility: hidden; + top: 40px; + left: 0; + margin: 0; + padding: 0; + opacity: 0; + list-style: none; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); + transition: visibility 0s 0.2s, opacity 0.2s; +} +.tsd-select .tsd-select-list li { + padding: 0 20px 0 0; + background-color: var(--color-background); +} +.tsd-select .tsd-select-list li:before { + background-position: 40px 0; +} +.tsd-select .tsd-select-list li:nth-child(even) { + background-color: var(--color-panel); +} +.tsd-select .tsd-select-list li:hover { + background-color: var(--color-panel-divider); +} +.tsd-select .tsd-select-list li.selected:before { + background-position: -200px 0; +} +@media (max-width: 900px) { + .tsd-select .tsd-select-list { + top: 0; + left: auto; + right: 100%; + margin-right: -5px; + } + .tsd-select .tsd-select-label:before { + background-position: -280px 0; + } +} + +img { + max-width: 100%; +} + +.tsd-anchor-icon { + margin-left: 10px; + vertical-align: middle; + color: var(--color-text); +} + +.tsd-anchor-icon svg { + width: 1em; + height: 1em; + visibility: hidden; +} + +.tsd-anchor-link:hover > .tsd-anchor-icon svg { + visibility: visible; +} diff --git a/frontend/docs/assets/widgets.png b/frontend/docs/assets/widgets.png new file mode 100644 index 0000000000000000000000000000000000000000..c7380532ac1b45400620011c37c4dcb7aec27a4c GIT binary patch literal 480 zcmeAS@N?(olHy`uVBq!ia0y~yU~~YoH8@y+q^jrZML>b&o-U3d6^w6h1+IPUz|;DW zIZ;96kdsD>Qv^q=09&hp0GpEni<1IR%gvP3v%OR9*{MuRTKWHZyIbuBt)Ci`cU_&% z1T+i^Y)o{%281-<3TpPAUTzw5v;RY=>1rvxmPl96#kYc9hX!6V^nB|ad#(S+)}?8C zr_H+lT3B#So$T=?$(w3-{rbQ4R<@nsf$}$hwSO)A$8&`(j+wQf=Jwhb0`CvhR5DCf z^OgI)KQemrUFPH+UynC$Y~QHG%DbTVh-Skz{enNU)cV_hPu~{TD7TPZl>0&K>iuE| z7AYn$7)Jrb9GE&SfQW4q&G*@N|4cHI`VakFa5-C!ov&XD)J(qp$rJJ*9e z-sHv}#g*T7Cv048d1v~BEAzM5FztAse#q78WWC^BUCzQ U&wLp6h6BX&boFyt=akR{0G%$)mH+?% literal 0 HcmV?d00001 diff --git a/frontend/docs/assets/widgets@2x.png b/frontend/docs/assets/widgets@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4bbbd57272f3b28f47527d4951ad10f950b8ad43 GIT binary patch literal 855 zcmeAS@N?(olHy`uVBq!ia0y~yU}^xe12~w0Jcmn z@(X6T|9^jgLcx21{)7exgY)a>N6m2F0<`Rqr;B4q1>>88jUdw-7W`c)zLE*mq8W2H z-<&Jl_Hco5BuC5n@AbF5GD82~-e8-v=#zCyUX0F-o}8pPfAv`!GN$ff+TL<~@kgt} z62eO?_|&+>xBmM$@p|z`tIKEdpPf8%qI>4r7@jn<=eta*{3~?g(zz{Ke9zc-G^gr? z-7foa?LcS!hmbwzru}ICvbWLlW8;+l-}!^=c32!^nV`+`C*;0-*Y%l94pC;Cb3GXz zzSf%a!{gVr{Y_lVuUj+a)*Ca+!-Hu%xmP&&X-2CuANY8^i{D7Kg6qzP zXz_ps9+lN8ESH{K4`yu&b~I>N9xGlE&;2u*b?+Go!AhN?m-bxlLvtC#MzDF2kFzfHJ1W7ybqdefSqVhbOykd*Yi%EDuhs z4wF{ft^bv2+DDnKb8gj1FuvcV`M}luS>lO<^)8x>y1#R;a=-ZKwWTQQb)ioBbi;zh zD!f5V)8581to1LL7c9!l^PSC$NBPYif!_vAZhmL4)v4U)4UsrLYiH_9rmQDd?)(e5 z^pcH>qvBg*i0dus2r*mp4;zKvu=P#s-ti;2obl`NjjwoYd>e(oo#j_uyRb<7Pv^If zzZ|mGHmV)8^tbO%^>eqMw(@7(&3g{jEp-Najo7V75xI_ZHK*FA`elF{r5}E*d7+j_R literal 0 HcmV?d00001 diff --git a/frontend/docs/enums/Data.Enums.Role.html b/frontend/docs/enums/Data.Enums.Role.html new file mode 100644 index 000000000..89faed224 --- /dev/null +++ b/frontend/docs/enums/Data.Enums.Role.html @@ -0,0 +1,4 @@ +Role | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Enumeration Role

+

Enum for the different levels of authority a user +can have

+

Index

Enumeration members

Enumeration members

ADMIN = 0
COACH = 1

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/enums/Data.Enums.StorageKey.html b/frontend/docs/enums/Data.Enums.StorageKey.html new file mode 100644 index 000000000..45ef7ed20 --- /dev/null +++ b/frontend/docs/enums/Data.Enums.StorageKey.html @@ -0,0 +1,3 @@ +StorageKey | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Enumeration StorageKey

+

Keys in LocalStorage

+

Index

Enumeration members

Enumeration members

BEARER_TOKEN = "bearerToken"

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/index.html b/frontend/docs/index.html new file mode 100644 index 000000000..6cdff7137 --- /dev/null +++ b/frontend/docs/index.html @@ -0,0 +1,107 @@ +frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

frontend

+ +

Frontend

+
+ + +

Installing (and using) Yarn

+
+

- What is my purpose?

+

- You install Yarn

+
npm install --global yarn
+
+

:heavy_exclamation_mark: Do not use npm anymore! Yarn and npm shouldn't be used at the same time.

+
# Installing new package
yarn add <package_name>

# Installing new package as a dev dependency
yarn add --dev <package_name>

# Installing all packages listed in package.json
yarn install +
+ + +

Setting up Prettier and ESLint

+
+

This directory contains configuration files for Prettier and ESLint, and depending on your IDE you may have to install or configure these in order for this to work.

+ + +

Prettier

+
+

Prettier is a code formatter that enforces your code to follow a specific style. Examples include automatically adding semicolons (;) at the end of every line, converting single-quoted strings ('a') to double-quoted strings ("a"), etc.

+ + +

ESLint

+
+

ESLint is, as the name suggests, a linter that reviews your code for bad practices and ugly constructions.

+ + +

JetBrains WebStorm

+
+

When using WebStorm, Prettier and ESLint are supported by default. ESLint is enabled automatically if a .eslintrc file is present, but you do have to enable Prettier in the settings.

+
    +
  1. Make sure the packages were installed by running yarn install, as Prettier and ESLint are among them.
  2. +
  3. In the search bar, type in "Prettier" (or navigate to Languages & Frameworks > JavaScript > Prettier manually).
  4. +
  5. If the Prettier package-field is still empty, click the dropdown. WebStorm should automatically list the Prettier from your local node-modules directory.
  6. +
  7. Select the On 'Reformat Code' action and On save checkboxes.
  8. +
+

Prettier WebStorm configuration

+ + +

Visual Studio Code

+
+

Visual Studio Code requires an extension for Prettier and ESLint to work, as they are not present in the editor.

+
    +
  1. Make sure the packages were installed by running yarn install, as Prettier and ESLint are among them

    +
  2. +
  3. Install the Prettier extension.

    +
  4. +
  5. Install the ESLint extension.

    +
  6. +
  7. Select Prettier as the default formatter in the Editor: Default Formatter dropdown option.

    +

    VSCode: Default Formatter setting

    +
  8. +
  9. Enable the Editor: Format On Save option.

    +

    VSCode: Format On Save setting

    +
  10. +
  11. The path to the Prettier config file, and the module in node_modules should be detected automatically. In case it isn't (see Try it out!, you can always fill in the fields in Prettier: Config Path and Prettier: Prettier Path.

    +
  12. +
+ + +

Try it out!

+
+

To test if your new settings work, you can try the following:

+
    +
  1. Create a new TypeScript file, with any name (for example test.ts)

    +
  2. +
  3. In that file, add the following piece of code:

    +
    export const x = 5 // Don't add a semicolon here

    export function test() {
    // "variable" is never used, and never reassigned
    let variable = "something";
    } +
    +
  4. +
  5. Save the file by pressing ctrl + s

    +
  6. +
  7. Prettier: you should see a semicolon being added at the end of the line automatically

    +
  8. +
  9. ESLint: you should get a warning on variable telling you that it was never used, and also that it should be marked as const because it's never reassigned.

    +
  10. +
  11. Don't forget to remove the test.ts file again :)

    +
  12. +
+ + +

Available Scripts

+
+

In the project directory, you can run:

+ + +

yarn start

+
+

Runs the app in the development mode.
Open http://localhost:3000 to view it in the browser.

+

The page will reload if you make edits.
You will also see any lint errors in the console.

+ + +

yarn test

+
+

Launches the test runner.

+ + +

yarn build

+
+

Builds the app for production to the build folder. It correctly bundles React in production mode and optimizes the build for the best performance.

+

The build is minified and the filenames include the hashes.

+

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/interfaces/Contexts.AuthContextState.html b/frontend/docs/interfaces/Contexts.AuthContextState.html new file mode 100644 index 000000000..75722106a --- /dev/null +++ b/frontend/docs/interfaces/Contexts.AuthContextState.html @@ -0,0 +1 @@ +AuthContextState | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface AuthContextState

Hierarchy

  • AuthContextState

Index

Properties

editions: number[]
isLoggedIn: null | boolean
role: null | Role
token: null | string

Methods

  • setEditions(value: number[]): void
  • setIsLoggedIn(value: null | boolean): void
  • setRole(value: null | Role): void
  • setToken(value: null | string): void

Legend

  • Property
  • Method

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/interfaces/Data.Interfaces.User.html b/frontend/docs/interfaces/Data.Interfaces.User.html new file mode 100644 index 000000000..1a91fa76e --- /dev/null +++ b/frontend/docs/interfaces/Data.Interfaces.User.html @@ -0,0 +1 @@ +User | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • User

Index

Properties

admin: boolean
editions: string[]
name: string
userId: number

Legend

  • Property

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules.html b/frontend/docs/modules.html new file mode 100644 index 000000000..fee4bb2ff --- /dev/null +++ b/frontend/docs/modules.html @@ -0,0 +1 @@ +frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

frontend

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Components.Login.html b/frontend/docs/modules/Components.Login.html new file mode 100644 index 000000000..b6fd1ef18 --- /dev/null +++ b/frontend/docs/modules/Components.Login.html @@ -0,0 +1 @@ +Login | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Functions

  • Email(__namedParameters: { email: string; setEmail: any }): Element
  • Password(__namedParameters: { password: string; callLogIn: any; setPassword: any }): Element
  • SocialButtons(): Element
  • WelcomeText(): Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Components.Register.html b/frontend/docs/modules/Components.Register.html new file mode 100644 index 000000000..fdcd6cb4a --- /dev/null +++ b/frontend/docs/modules/Components.Register.html @@ -0,0 +1 @@ +Register | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Register

Index

Functions

  • BadInviteLink(): Element
  • ConfirmPassword(__namedParameters: { confirmPassword: string; callRegister: any; setConfirmPassword: any }): Element
  • Email(__namedParameters: { email: string; setEmail: any }): Element
  • InfoText(): Element
  • Name(__namedParameters: { name: string; setName: any }): Element
  • Password(__namedParameters: { password: string; setPassword: any }): Element
  • SocialButtons(): Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Components.html b/frontend/docs/modules/Components.html new file mode 100644 index 000000000..a2955d48b --- /dev/null +++ b/frontend/docs/modules/Components.html @@ -0,0 +1,7 @@ +Components | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Components

Index

Functions

  • AdminRoute(): Element
  • Footer(): Element
  • NavBar(): Element
  • OSOCLetters(): Element
  • PrivateRoute(): Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Contexts.html b/frontend/docs/modules/Contexts.html new file mode 100644 index 000000000..a6a9b5228 --- /dev/null +++ b/frontend/docs/modules/Contexts.html @@ -0,0 +1,6 @@ +Contexts | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Contexts

Index

Interfaces

Functions

Functions

  • AuthProvider(__namedParameters: { children: ReactNode }): Element
  • +

    Provider for auth that creates getters, setters, maintains state, and +provides default values

    +

    Not strictly necessary but keeps the main App clean by handling this +code here instead

    +

    Parameters

    • __namedParameters: { children: ReactNode }
      • children: ReactNode

    Returns Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Data.Enums.html b/frontend/docs/modules/Data.Enums.html new file mode 100644 index 000000000..62742a807 --- /dev/null +++ b/frontend/docs/modules/Data.Enums.html @@ -0,0 +1 @@ +Enums | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Enums

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Data.Interfaces.html b/frontend/docs/modules/Data.Interfaces.html new file mode 100644 index 000000000..904b54260 --- /dev/null +++ b/frontend/docs/modules/Data.Interfaces.html @@ -0,0 +1 @@ +Interfaces | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Interfaces

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Data.html b/frontend/docs/modules/Data.html new file mode 100644 index 000000000..138e0db44 --- /dev/null +++ b/frontend/docs/modules/Data.html @@ -0,0 +1 @@ +Data | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Data

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 57b1ecc33..51a247d9e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,12 +47,14 @@ "eslint-plugin-react": "^7.28.0", "eslint-plugin-standard": "^5.0.0", "jest": "^27.5.1", - "prettier": "^2.5.1" + "prettier": "^2.5.1", + "typedoc": "^0.22.13" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "lint": "eslint . --ext js,ts", + "docs": "typedoc", "test": "react-scripts test --watchAll=false" }, "eslintConfig": { diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts new file mode 100644 index 000000000..546f604dc --- /dev/null +++ b/frontend/src/components/index.ts @@ -0,0 +1,7 @@ +export { default as AdminRoute } from "./AdminRoute"; +export { default as Footer } from "./Footer"; +export * as Login from "./LoginComponents"; +export { default as NavBar } from "./navbar"; +export { default as OSOCLetters } from "./OSOCLetters"; +export { default as PrivateRoute } from "./PrivateRoute"; +export * as Register from "./RegisterComponents"; diff --git a/frontend/src/data/index.ts b/frontend/src/data/index.ts new file mode 100644 index 000000000..36c589c64 --- /dev/null +++ b/frontend/src/data/index.ts @@ -0,0 +1,2 @@ +export * as Enums from "./enums"; +export * as Interfaces from "./interfaces"; diff --git a/frontend/src/index.ts b/frontend/src/index.ts new file mode 100644 index 000000000..e0af47457 --- /dev/null +++ b/frontend/src/index.ts @@ -0,0 +1,3 @@ +export * as Components from "./components"; +export * as Contexts from "./contexts"; +export * as Data from "./data"; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index a273b0cfc..1798379bd 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,26 +1,32 @@ { - "compilerOptions": { - "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" ], - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx" - }, - "include": [ - "src" - ] + "typedocOptions": { + "entryPoints": [ + "src/" + ], + "out": "docs" + } } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 9cb056679..1fb299499 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2923,6 +2923,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + 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" @@ -4843,7 +4850,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" 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.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" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -6028,6 +6035,11 @@ json5@^2.1.2, json5@^2.2.0: dependencies: minimist "^1.2.5" +jsonc-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" + integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -6232,6 +6244,11 @@ lru-cache@^6.0.0: 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" + 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" @@ -6258,6 +6275,11 @@ makeerror@1.0.12: 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" + 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" @@ -6361,6 +6383,13 @@ minimatch@^3.0.4, minimatch@^3.1.2: 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" + 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" @@ -8232,6 +8261,15 @@ shell-quote@^1.7.3: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" 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" + integrity sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng== + dependencies: + jsonc-parser "^3.0.0" + vscode-oniguruma "^1.6.1" + vscode-textmate "5.2.0" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -8861,6 +8899,17 @@ typedarray-to-buffer@^3.1.5: 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== + dependencies: + glob "^7.2.0" + lunr "^2.3.9" + marked "^4.0.12" + 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" @@ -8997,6 +9046,16 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 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" + 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" + 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" From 36812c0d88c04b87dcc6690196f75cbf88b28b8b Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 3 Apr 2022 00:34:54 +0200 Subject: [PATCH 213/536] Add all index files & exports --- frontend/docs/assets/search.js | 2 +- frontend/docs/enums/Data.Enums.Role.html | 2 +- frontend/docs/enums/Data.Enums.StorageKey.html | 2 +- frontend/docs/index.html | 2 +- frontend/docs/interfaces/Contexts.AuthContextState.html | 2 +- frontend/docs/interfaces/Data.Interfaces.User.html | 2 +- frontend/docs/modules.html | 2 +- frontend/docs/modules/Components.Login.html | 2 +- frontend/docs/modules/Components.Register.html | 2 +- frontend/docs/modules/Components.html | 6 +++--- frontend/docs/modules/Contexts.html | 4 ++-- frontend/docs/modules/Data.Enums.html | 2 +- frontend/docs/modules/Data.Interfaces.html | 2 +- frontend/docs/modules/Data.html | 2 +- frontend/docs/modules/Utils.Api.html | 6 ++++++ frontend/docs/modules/Utils.LocalStorage.html | 6 ++++++ frontend/docs/modules/Utils.html | 1 + frontend/docs/modules/Views.Errors.html | 1 + frontend/docs/modules/Views.html | 1 + frontend/src/index.ts | 2 ++ frontend/src/utils/index.ts | 2 ++ frontend/src/views/index.ts | 8 ++++++++ 22 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 frontend/docs/modules/Utils.Api.html create mode 100644 frontend/docs/modules/Utils.LocalStorage.html create mode 100644 frontend/docs/modules/Utils.html create mode 100644 frontend/docs/modules/Views.Errors.html create mode 100644 frontend/docs/modules/Views.html create mode 100644 frontend/src/utils/index.ts create mode 100644 frontend/src/views/index.ts diff --git a/frontend/docs/assets/search.js b/frontend/docs/assets/search.js index 2b931b200..52dd6c057 100644 --- a/frontend/docs/assets/search.js +++ b/frontend/docs/assets/search.js @@ -1 +1 @@ -window.searchData = JSON.parse("{\"kinds\":{\"4\":\"Namespace\",\"8\":\"Enumeration\",\"16\":\"Enumeration member\",\"64\":\"Function\",\"256\":\"Interface\",\"1024\":\"Property\",\"2048\":\"Method\"},\"rows\":[{\"id\":0,\"kind\":4,\"name\":\"Components\",\"url\":\"modules/Components.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":1,\"kind\":64,\"name\":\"AdminRoute\",\"url\":\"modules/Components.html#AdminRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":2,\"kind\":64,\"name\":\"Footer\",\"url\":\"modules/Components.html#Footer\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":3,\"kind\":4,\"name\":\"Login\",\"url\":\"modules/Components.Login.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":4,\"kind\":64,\"name\":\"WelcomeText\",\"url\":\"modules/Components.Login.html#WelcomeText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Login\"},{\"id\":5,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/Components.Login.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Login\"},{\"id\":6,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/Components.Login.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Login\"},{\"id\":7,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/Components.Login.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Login\"},{\"id\":8,\"kind\":64,\"name\":\"NavBar\",\"url\":\"modules/Components.html#NavBar\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":9,\"kind\":64,\"name\":\"OSOCLetters\",\"url\":\"modules/Components.html#OSOCLetters\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":10,\"kind\":64,\"name\":\"PrivateRoute\",\"url\":\"modules/Components.html#PrivateRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":11,\"kind\":4,\"name\":\"Register\",\"url\":\"modules/Components.Register.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":12,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/Components.Register.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":13,\"kind\":64,\"name\":\"Name\",\"url\":\"modules/Components.Register.html#Name\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":14,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/Components.Register.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":15,\"kind\":64,\"name\":\"ConfirmPassword\",\"url\":\"modules/Components.Register.html#ConfirmPassword\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":16,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/Components.Register.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":17,\"kind\":64,\"name\":\"InfoText\",\"url\":\"modules/Components.Register.html#InfoText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":18,\"kind\":64,\"name\":\"BadInviteLink\",\"url\":\"modules/Components.Register.html#BadInviteLink\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":19,\"kind\":4,\"name\":\"Contexts\",\"url\":\"modules/Contexts.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":20,\"kind\":256,\"name\":\"AuthContextState\",\"url\":\"interfaces/Contexts.AuthContextState.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"Contexts\"},{\"id\":21,\"kind\":1024,\"name\":\"isLoggedIn\",\"url\":\"interfaces/Contexts.AuthContextState.html#isLoggedIn\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":22,\"kind\":2048,\"name\":\"setIsLoggedIn\",\"url\":\"interfaces/Contexts.AuthContextState.html#setIsLoggedIn\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":23,\"kind\":1024,\"name\":\"role\",\"url\":\"interfaces/Contexts.AuthContextState.html#role\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":24,\"kind\":2048,\"name\":\"setRole\",\"url\":\"interfaces/Contexts.AuthContextState.html#setRole\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":25,\"kind\":1024,\"name\":\"token\",\"url\":\"interfaces/Contexts.AuthContextState.html#token\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":26,\"kind\":2048,\"name\":\"setToken\",\"url\":\"interfaces/Contexts.AuthContextState.html#setToken\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":27,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/Contexts.AuthContextState.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":28,\"kind\":2048,\"name\":\"setEditions\",\"url\":\"interfaces/Contexts.AuthContextState.html#setEditions\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":29,\"kind\":64,\"name\":\"AuthProvider\",\"url\":\"modules/Contexts.html#AuthProvider\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Contexts\"},{\"id\":30,\"kind\":4,\"name\":\"Data\",\"url\":\"modules/Data.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":31,\"kind\":4,\"name\":\"Enums\",\"url\":\"modules/Data.Enums.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Data\"},{\"id\":32,\"kind\":8,\"name\":\"StorageKey\",\"url\":\"enums/Data.Enums.StorageKey.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"Data.Enums\"},{\"id\":33,\"kind\":16,\"name\":\"BEARER_TOKEN\",\"url\":\"enums/Data.Enums.StorageKey.html#BEARER_TOKEN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"Data.Enums.StorageKey\"},{\"id\":34,\"kind\":8,\"name\":\"Role\",\"url\":\"enums/Data.Enums.Role.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"Data.Enums\"},{\"id\":35,\"kind\":16,\"name\":\"ADMIN\",\"url\":\"enums/Data.Enums.Role.html#ADMIN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"Data.Enums.Role\"},{\"id\":36,\"kind\":16,\"name\":\"COACH\",\"url\":\"enums/Data.Enums.Role.html#COACH\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"Data.Enums.Role\"},{\"id\":37,\"kind\":4,\"name\":\"Interfaces\",\"url\":\"modules/Data.Interfaces.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Data\"},{\"id\":38,\"kind\":256,\"name\":\"User\",\"url\":\"interfaces/Data.Interfaces.User.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"Data.Interfaces\"},{\"id\":39,\"kind\":1024,\"name\":\"userId\",\"url\":\"interfaces/Data.Interfaces.User.html#userId\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Data.Interfaces.User\"},{\"id\":40,\"kind\":1024,\"name\":\"name\",\"url\":\"interfaces/Data.Interfaces.User.html#name\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Data.Interfaces.User\"},{\"id\":41,\"kind\":1024,\"name\":\"admin\",\"url\":\"interfaces/Data.Interfaces.User.html#admin\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Data.Interfaces.User\"},{\"id\":42,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/Data.Interfaces.User.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Data.Interfaces.User\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"parent\"],\"fieldVectors\":[[\"name/0\",[0,16.441]],[\"parent/0\",[]],[\"name/1\",[1,33.787]],[\"parent/1\",[0,1.595]],[\"name/2\",[2,33.787]],[\"parent/2\",[0,1.595]],[\"name/3\",[3,33.787]],[\"parent/3\",[0,1.595]],[\"name/4\",[4,33.787]],[\"parent/4\",[5,2.212]],[\"name/5\",[6,28.679]],[\"parent/5\",[5,2.212]],[\"name/6\",[7,28.679]],[\"parent/6\",[5,2.212]],[\"name/7\",[8,28.679]],[\"parent/7\",[5,2.212]],[\"name/8\",[9,33.787]],[\"parent/8\",[0,1.595]],[\"name/9\",[10,33.787]],[\"parent/9\",[0,1.595]],[\"name/10\",[11,33.787]],[\"parent/10\",[0,1.595]],[\"name/11\",[12,33.787]],[\"parent/11\",[0,1.595]],[\"name/12\",[8,28.679]],[\"parent/12\",[13,1.717]],[\"name/13\",[14,28.679]],[\"parent/13\",[13,1.717]],[\"name/14\",[7,28.679]],[\"parent/14\",[13,1.717]],[\"name/15\",[15,33.787]],[\"parent/15\",[13,1.717]],[\"name/16\",[6,28.679]],[\"parent/16\",[13,1.717]],[\"name/17\",[16,33.787]],[\"parent/17\",[13,1.717]],[\"name/18\",[17,33.787]],[\"parent/18\",[13,1.717]],[\"name/19\",[18,25.314]],[\"parent/19\",[]],[\"name/20\",[19,33.787]],[\"parent/20\",[18,2.456]],[\"name/21\",[20,33.787]],[\"parent/21\",[21,1.595]],[\"name/22\",[22,33.787]],[\"parent/22\",[21,1.595]],[\"name/23\",[23,28.679]],[\"parent/23\",[21,1.595]],[\"name/24\",[24,33.787]],[\"parent/24\",[21,1.595]],[\"name/25\",[25,33.787]],[\"parent/25\",[21,1.595]],[\"name/26\",[26,33.787]],[\"parent/26\",[21,1.595]],[\"name/27\",[27,28.679]],[\"parent/27\",[21,1.595]],[\"name/28\",[28,33.787]],[\"parent/28\",[21,1.595]],[\"name/29\",[29,33.787]],[\"parent/29\",[18,2.456]],[\"name/30\",[30,25.314]],[\"parent/30\",[]],[\"name/31\",[31,33.787]],[\"parent/31\",[30,2.456]],[\"name/32\",[32,33.787]],[\"parent/32\",[33,2.783]],[\"name/33\",[34,33.787]],[\"parent/33\",[35,3.278]],[\"name/34\",[23,28.679]],[\"parent/34\",[33,2.783]],[\"name/35\",[36,28.679]],[\"parent/35\",[37,2.783]],[\"name/36\",[38,33.787]],[\"parent/36\",[37,2.783]],[\"name/37\",[39,33.787]],[\"parent/37\",[30,2.456]],[\"name/38\",[40,33.787]],[\"parent/38\",[41,3.278]],[\"name/39\",[42,33.787]],[\"parent/39\",[43,2.212]],[\"name/40\",[14,28.679]],[\"parent/40\",[43,2.212]],[\"name/41\",[36,28.679]],[\"parent/41\",[43,2.212]],[\"name/42\",[27,28.679]],[\"parent/42\",[43,2.212]]],\"invertedIndex\":[[\"admin\",{\"_index\":36,\"name\":{\"35\":{},\"41\":{}},\"parent\":{}}],[\"adminroute\",{\"_index\":1,\"name\":{\"1\":{}},\"parent\":{}}],[\"authcontextstate\",{\"_index\":19,\"name\":{\"20\":{}},\"parent\":{}}],[\"authprovider\",{\"_index\":29,\"name\":{\"29\":{}},\"parent\":{}}],[\"badinvitelink\",{\"_index\":17,\"name\":{\"18\":{}},\"parent\":{}}],[\"bearer_token\",{\"_index\":34,\"name\":{\"33\":{}},\"parent\":{}}],[\"coach\",{\"_index\":38,\"name\":{\"36\":{}},\"parent\":{}}],[\"components\",{\"_index\":0,\"name\":{\"0\":{}},\"parent\":{\"1\":{},\"2\":{},\"3\":{},\"8\":{},\"9\":{},\"10\":{},\"11\":{}}}],[\"components.login\",{\"_index\":5,\"name\":{},\"parent\":{\"4\":{},\"5\":{},\"6\":{},\"7\":{}}}],[\"components.register\",{\"_index\":13,\"name\":{},\"parent\":{\"12\":{},\"13\":{},\"14\":{},\"15\":{},\"16\":{},\"17\":{},\"18\":{}}}],[\"confirmpassword\",{\"_index\":15,\"name\":{\"15\":{}},\"parent\":{}}],[\"contexts\",{\"_index\":18,\"name\":{\"19\":{}},\"parent\":{\"20\":{},\"29\":{}}}],[\"contexts.authcontextstate\",{\"_index\":21,\"name\":{},\"parent\":{\"21\":{},\"22\":{},\"23\":{},\"24\":{},\"25\":{},\"26\":{},\"27\":{},\"28\":{}}}],[\"data\",{\"_index\":30,\"name\":{\"30\":{}},\"parent\":{\"31\":{},\"37\":{}}}],[\"data.enums\",{\"_index\":33,\"name\":{},\"parent\":{\"32\":{},\"34\":{}}}],[\"data.enums.role\",{\"_index\":37,\"name\":{},\"parent\":{\"35\":{},\"36\":{}}}],[\"data.enums.storagekey\",{\"_index\":35,\"name\":{},\"parent\":{\"33\":{}}}],[\"data.interfaces\",{\"_index\":41,\"name\":{},\"parent\":{\"38\":{}}}],[\"data.interfaces.user\",{\"_index\":43,\"name\":{},\"parent\":{\"39\":{},\"40\":{},\"41\":{},\"42\":{}}}],[\"editions\",{\"_index\":27,\"name\":{\"27\":{},\"42\":{}},\"parent\":{}}],[\"email\",{\"_index\":8,\"name\":{\"7\":{},\"12\":{}},\"parent\":{}}],[\"enums\",{\"_index\":31,\"name\":{\"31\":{}},\"parent\":{}}],[\"footer\",{\"_index\":2,\"name\":{\"2\":{}},\"parent\":{}}],[\"infotext\",{\"_index\":16,\"name\":{\"17\":{}},\"parent\":{}}],[\"interfaces\",{\"_index\":39,\"name\":{\"37\":{}},\"parent\":{}}],[\"isloggedin\",{\"_index\":20,\"name\":{\"21\":{}},\"parent\":{}}],[\"login\",{\"_index\":3,\"name\":{\"3\":{}},\"parent\":{}}],[\"name\",{\"_index\":14,\"name\":{\"13\":{},\"40\":{}},\"parent\":{}}],[\"navbar\",{\"_index\":9,\"name\":{\"8\":{}},\"parent\":{}}],[\"osocletters\",{\"_index\":10,\"name\":{\"9\":{}},\"parent\":{}}],[\"password\",{\"_index\":7,\"name\":{\"6\":{},\"14\":{}},\"parent\":{}}],[\"privateroute\",{\"_index\":11,\"name\":{\"10\":{}},\"parent\":{}}],[\"register\",{\"_index\":12,\"name\":{\"11\":{}},\"parent\":{}}],[\"role\",{\"_index\":23,\"name\":{\"23\":{},\"34\":{}},\"parent\":{}}],[\"seteditions\",{\"_index\":28,\"name\":{\"28\":{}},\"parent\":{}}],[\"setisloggedin\",{\"_index\":22,\"name\":{\"22\":{}},\"parent\":{}}],[\"setrole\",{\"_index\":24,\"name\":{\"24\":{}},\"parent\":{}}],[\"settoken\",{\"_index\":26,\"name\":{\"26\":{}},\"parent\":{}}],[\"socialbuttons\",{\"_index\":6,\"name\":{\"5\":{},\"16\":{}},\"parent\":{}}],[\"storagekey\",{\"_index\":32,\"name\":{\"32\":{}},\"parent\":{}}],[\"token\",{\"_index\":25,\"name\":{\"25\":{}},\"parent\":{}}],[\"user\",{\"_index\":40,\"name\":{\"38\":{}},\"parent\":{}}],[\"userid\",{\"_index\":42,\"name\":{\"39\":{}},\"parent\":{}}],[\"welcometext\",{\"_index\":4,\"name\":{\"4\":{}},\"parent\":{}}]],\"pipeline\":[]}}"); \ No newline at end of file +window.searchData = JSON.parse("{\"kinds\":{\"4\":\"Namespace\",\"8\":\"Enumeration\",\"16\":\"Enumeration member\",\"64\":\"Function\",\"256\":\"Interface\",\"1024\":\"Property\",\"2048\":\"Method\"},\"rows\":[{\"id\":0,\"kind\":4,\"name\":\"Components\",\"url\":\"modules/Components.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":1,\"kind\":64,\"name\":\"AdminRoute\",\"url\":\"modules/Components.html#AdminRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":2,\"kind\":64,\"name\":\"Footer\",\"url\":\"modules/Components.html#Footer\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":3,\"kind\":4,\"name\":\"Login\",\"url\":\"modules/Components.Login.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":4,\"kind\":64,\"name\":\"WelcomeText\",\"url\":\"modules/Components.Login.html#WelcomeText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Login\"},{\"id\":5,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/Components.Login.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Login\"},{\"id\":6,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/Components.Login.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Login\"},{\"id\":7,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/Components.Login.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Login\"},{\"id\":8,\"kind\":64,\"name\":\"NavBar\",\"url\":\"modules/Components.html#NavBar\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":9,\"kind\":64,\"name\":\"OSOCLetters\",\"url\":\"modules/Components.html#OSOCLetters\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":10,\"kind\":64,\"name\":\"PrivateRoute\",\"url\":\"modules/Components.html#PrivateRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":11,\"kind\":4,\"name\":\"Register\",\"url\":\"modules/Components.Register.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":12,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/Components.Register.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":13,\"kind\":64,\"name\":\"Name\",\"url\":\"modules/Components.Register.html#Name\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":14,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/Components.Register.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":15,\"kind\":64,\"name\":\"ConfirmPassword\",\"url\":\"modules/Components.Register.html#ConfirmPassword\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":16,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/Components.Register.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":17,\"kind\":64,\"name\":\"InfoText\",\"url\":\"modules/Components.Register.html#InfoText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":18,\"kind\":64,\"name\":\"BadInviteLink\",\"url\":\"modules/Components.Register.html#BadInviteLink\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":19,\"kind\":4,\"name\":\"Contexts\",\"url\":\"modules/Contexts.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":20,\"kind\":256,\"name\":\"AuthContextState\",\"url\":\"interfaces/Contexts.AuthContextState.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"Contexts\"},{\"id\":21,\"kind\":1024,\"name\":\"isLoggedIn\",\"url\":\"interfaces/Contexts.AuthContextState.html#isLoggedIn\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":22,\"kind\":2048,\"name\":\"setIsLoggedIn\",\"url\":\"interfaces/Contexts.AuthContextState.html#setIsLoggedIn\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":23,\"kind\":1024,\"name\":\"role\",\"url\":\"interfaces/Contexts.AuthContextState.html#role\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":24,\"kind\":2048,\"name\":\"setRole\",\"url\":\"interfaces/Contexts.AuthContextState.html#setRole\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":25,\"kind\":1024,\"name\":\"token\",\"url\":\"interfaces/Contexts.AuthContextState.html#token\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":26,\"kind\":2048,\"name\":\"setToken\",\"url\":\"interfaces/Contexts.AuthContextState.html#setToken\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":27,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/Contexts.AuthContextState.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":28,\"kind\":2048,\"name\":\"setEditions\",\"url\":\"interfaces/Contexts.AuthContextState.html#setEditions\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":29,\"kind\":64,\"name\":\"AuthProvider\",\"url\":\"modules/Contexts.html#AuthProvider\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Contexts\"},{\"id\":30,\"kind\":4,\"name\":\"Data\",\"url\":\"modules/Data.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":31,\"kind\":4,\"name\":\"Enums\",\"url\":\"modules/Data.Enums.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Data\"},{\"id\":32,\"kind\":8,\"name\":\"StorageKey\",\"url\":\"enums/Data.Enums.StorageKey.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"Data.Enums\"},{\"id\":33,\"kind\":16,\"name\":\"BEARER_TOKEN\",\"url\":\"enums/Data.Enums.StorageKey.html#BEARER_TOKEN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"Data.Enums.StorageKey\"},{\"id\":34,\"kind\":8,\"name\":\"Role\",\"url\":\"enums/Data.Enums.Role.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"Data.Enums\"},{\"id\":35,\"kind\":16,\"name\":\"ADMIN\",\"url\":\"enums/Data.Enums.Role.html#ADMIN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"Data.Enums.Role\"},{\"id\":36,\"kind\":16,\"name\":\"COACH\",\"url\":\"enums/Data.Enums.Role.html#COACH\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"Data.Enums.Role\"},{\"id\":37,\"kind\":4,\"name\":\"Interfaces\",\"url\":\"modules/Data.Interfaces.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Data\"},{\"id\":38,\"kind\":256,\"name\":\"User\",\"url\":\"interfaces/Data.Interfaces.User.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"Data.Interfaces\"},{\"id\":39,\"kind\":1024,\"name\":\"userId\",\"url\":\"interfaces/Data.Interfaces.User.html#userId\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Data.Interfaces.User\"},{\"id\":40,\"kind\":1024,\"name\":\"name\",\"url\":\"interfaces/Data.Interfaces.User.html#name\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Data.Interfaces.User\"},{\"id\":41,\"kind\":1024,\"name\":\"admin\",\"url\":\"interfaces/Data.Interfaces.User.html#admin\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Data.Interfaces.User\"},{\"id\":42,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/Data.Interfaces.User.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Data.Interfaces.User\"},{\"id\":43,\"kind\":4,\"name\":\"Utils\",\"url\":\"modules/Utils.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":44,\"kind\":4,\"name\":\"Api\",\"url\":\"modules/Utils.Api.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Utils\"},{\"id\":45,\"kind\":64,\"name\":\"validateRegistrationUrl\",\"url\":\"modules/Utils.Api.html#validateRegistrationUrl\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Utils.Api\"},{\"id\":46,\"kind\":64,\"name\":\"setBearerToken\",\"url\":\"modules/Utils.Api.html#setBearerToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Utils.Api\"},{\"id\":47,\"kind\":4,\"name\":\"LocalStorage\",\"url\":\"modules/Utils.LocalStorage.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Utils\"},{\"id\":48,\"kind\":64,\"name\":\"getToken\",\"url\":\"modules/Utils.LocalStorage.html#getToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Utils.LocalStorage\"},{\"id\":49,\"kind\":64,\"name\":\"setToken\",\"url\":\"modules/Utils.LocalStorage.html#setToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Utils.LocalStorage\"},{\"id\":50,\"kind\":4,\"name\":\"Views\",\"url\":\"modules/Views.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":51,\"kind\":4,\"name\":\"Errors\",\"url\":\"modules/Views.Errors.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Views\"},{\"id\":52,\"kind\":64,\"name\":\"NotFoundPage\",\"url\":\"modules/Views.Errors.html#NotFoundPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Views.Errors\"},{\"id\":53,\"kind\":64,\"name\":\"LoginPage\",\"url\":\"modules/Views.html#LoginPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Views\"},{\"id\":54,\"kind\":64,\"name\":\"PendingPage\",\"url\":\"modules/Views.html#PendingPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Views\"},{\"id\":55,\"kind\":64,\"name\":\"ProjectsPage\",\"url\":\"modules/Views.html#ProjectsPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Views\"},{\"id\":56,\"kind\":64,\"name\":\"RegisterPage\",\"url\":\"modules/Views.html#RegisterPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Views\"},{\"id\":57,\"kind\":64,\"name\":\"StudentsPage\",\"url\":\"modules/Views.html#StudentsPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Views\"},{\"id\":58,\"kind\":64,\"name\":\"UsersPage\",\"url\":\"modules/Views.html#UsersPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Views\"},{\"id\":59,\"kind\":64,\"name\":\"VerifyingTokenPage\",\"url\":\"modules/Views.html#VerifyingTokenPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Views\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"parent\"],\"fieldVectors\":[[\"name/0\",[0,19.708]],[\"parent/0\",[]],[\"name/1\",[1,37.054]],[\"parent/1\",[0,1.9]],[\"name/2\",[2,37.054]],[\"parent/2\",[0,1.9]],[\"name/3\",[3,37.054]],[\"parent/3\",[0,1.9]],[\"name/4\",[4,37.054]],[\"parent/4\",[5,2.513]],[\"name/5\",[6,31.946]],[\"parent/5\",[5,2.513]],[\"name/6\",[7,31.946]],[\"parent/6\",[5,2.513]],[\"name/7\",[8,31.946]],[\"parent/7\",[5,2.513]],[\"name/8\",[9,37.054]],[\"parent/8\",[0,1.9]],[\"name/9\",[10,37.054]],[\"parent/9\",[0,1.9]],[\"name/10\",[11,37.054]],[\"parent/10\",[0,1.9]],[\"name/11\",[12,37.054]],[\"parent/11\",[0,1.9]],[\"name/12\",[8,31.946]],[\"parent/12\",[13,2.021]],[\"name/13\",[14,31.946]],[\"parent/13\",[13,2.021]],[\"name/14\",[7,31.946]],[\"parent/14\",[13,2.021]],[\"name/15\",[15,37.054]],[\"parent/15\",[13,2.021]],[\"name/16\",[6,31.946]],[\"parent/16\",[13,2.021]],[\"name/17\",[16,37.054]],[\"parent/17\",[13,2.021]],[\"name/18\",[17,37.054]],[\"parent/18\",[13,2.021]],[\"name/19\",[18,28.581]],[\"parent/19\",[]],[\"name/20\",[19,37.054]],[\"parent/20\",[18,2.756]],[\"name/21\",[20,37.054]],[\"parent/21\",[21,1.9]],[\"name/22\",[22,37.054]],[\"parent/22\",[21,1.9]],[\"name/23\",[23,31.946]],[\"parent/23\",[21,1.9]],[\"name/24\",[24,37.054]],[\"parent/24\",[21,1.9]],[\"name/25\",[25,37.054]],[\"parent/25\",[21,1.9]],[\"name/26\",[26,31.946]],[\"parent/26\",[21,1.9]],[\"name/27\",[27,31.946]],[\"parent/27\",[21,1.9]],[\"name/28\",[28,37.054]],[\"parent/28\",[21,1.9]],[\"name/29\",[29,37.054]],[\"parent/29\",[18,2.756]],[\"name/30\",[30,28.581]],[\"parent/30\",[]],[\"name/31\",[31,37.054]],[\"parent/31\",[30,2.756]],[\"name/32\",[32,37.054]],[\"parent/32\",[33,3.08]],[\"name/33\",[34,37.054]],[\"parent/33\",[35,3.573]],[\"name/34\",[23,31.946]],[\"parent/34\",[33,3.08]],[\"name/35\",[36,31.946]],[\"parent/35\",[37,3.08]],[\"name/36\",[38,37.054]],[\"parent/36\",[37,3.08]],[\"name/37\",[39,37.054]],[\"parent/37\",[30,2.756]],[\"name/38\",[40,37.054]],[\"parent/38\",[41,3.573]],[\"name/39\",[42,37.054]],[\"parent/39\",[43,2.513]],[\"name/40\",[14,31.946]],[\"parent/40\",[43,2.513]],[\"name/41\",[36,31.946]],[\"parent/41\",[43,2.513]],[\"name/42\",[27,31.946]],[\"parent/42\",[43,2.513]],[\"name/43\",[44,28.581]],[\"parent/43\",[]],[\"name/44\",[45,37.054]],[\"parent/44\",[44,2.756]],[\"name/45\",[46,37.054]],[\"parent/45\",[47,3.08]],[\"name/46\",[48,37.054]],[\"parent/46\",[47,3.08]],[\"name/47\",[49,37.054]],[\"parent/47\",[44,2.756]],[\"name/48\",[50,37.054]],[\"parent/48\",[51,3.08]],[\"name/49\",[26,31.946]],[\"parent/49\",[51,3.08]],[\"name/50\",[52,18.596]],[\"parent/50\",[]],[\"name/51\",[53,37.054]],[\"parent/51\",[52,1.793]],[\"name/52\",[54,37.054]],[\"parent/52\",[55,3.573]],[\"name/53\",[56,37.054]],[\"parent/53\",[52,1.793]],[\"name/54\",[57,37.054]],[\"parent/54\",[52,1.793]],[\"name/55\",[58,37.054]],[\"parent/55\",[52,1.793]],[\"name/56\",[59,37.054]],[\"parent/56\",[52,1.793]],[\"name/57\",[60,37.054]],[\"parent/57\",[52,1.793]],[\"name/58\",[61,37.054]],[\"parent/58\",[52,1.793]],[\"name/59\",[62,37.054]],[\"parent/59\",[52,1.793]]],\"invertedIndex\":[[\"admin\",{\"_index\":36,\"name\":{\"35\":{},\"41\":{}},\"parent\":{}}],[\"adminroute\",{\"_index\":1,\"name\":{\"1\":{}},\"parent\":{}}],[\"api\",{\"_index\":45,\"name\":{\"44\":{}},\"parent\":{}}],[\"authcontextstate\",{\"_index\":19,\"name\":{\"20\":{}},\"parent\":{}}],[\"authprovider\",{\"_index\":29,\"name\":{\"29\":{}},\"parent\":{}}],[\"badinvitelink\",{\"_index\":17,\"name\":{\"18\":{}},\"parent\":{}}],[\"bearer_token\",{\"_index\":34,\"name\":{\"33\":{}},\"parent\":{}}],[\"coach\",{\"_index\":38,\"name\":{\"36\":{}},\"parent\":{}}],[\"components\",{\"_index\":0,\"name\":{\"0\":{}},\"parent\":{\"1\":{},\"2\":{},\"3\":{},\"8\":{},\"9\":{},\"10\":{},\"11\":{}}}],[\"components.login\",{\"_index\":5,\"name\":{},\"parent\":{\"4\":{},\"5\":{},\"6\":{},\"7\":{}}}],[\"components.register\",{\"_index\":13,\"name\":{},\"parent\":{\"12\":{},\"13\":{},\"14\":{},\"15\":{},\"16\":{},\"17\":{},\"18\":{}}}],[\"confirmpassword\",{\"_index\":15,\"name\":{\"15\":{}},\"parent\":{}}],[\"contexts\",{\"_index\":18,\"name\":{\"19\":{}},\"parent\":{\"20\":{},\"29\":{}}}],[\"contexts.authcontextstate\",{\"_index\":21,\"name\":{},\"parent\":{\"21\":{},\"22\":{},\"23\":{},\"24\":{},\"25\":{},\"26\":{},\"27\":{},\"28\":{}}}],[\"data\",{\"_index\":30,\"name\":{\"30\":{}},\"parent\":{\"31\":{},\"37\":{}}}],[\"data.enums\",{\"_index\":33,\"name\":{},\"parent\":{\"32\":{},\"34\":{}}}],[\"data.enums.role\",{\"_index\":37,\"name\":{},\"parent\":{\"35\":{},\"36\":{}}}],[\"data.enums.storagekey\",{\"_index\":35,\"name\":{},\"parent\":{\"33\":{}}}],[\"data.interfaces\",{\"_index\":41,\"name\":{},\"parent\":{\"38\":{}}}],[\"data.interfaces.user\",{\"_index\":43,\"name\":{},\"parent\":{\"39\":{},\"40\":{},\"41\":{},\"42\":{}}}],[\"editions\",{\"_index\":27,\"name\":{\"27\":{},\"42\":{}},\"parent\":{}}],[\"email\",{\"_index\":8,\"name\":{\"7\":{},\"12\":{}},\"parent\":{}}],[\"enums\",{\"_index\":31,\"name\":{\"31\":{}},\"parent\":{}}],[\"errors\",{\"_index\":53,\"name\":{\"51\":{}},\"parent\":{}}],[\"footer\",{\"_index\":2,\"name\":{\"2\":{}},\"parent\":{}}],[\"gettoken\",{\"_index\":50,\"name\":{\"48\":{}},\"parent\":{}}],[\"infotext\",{\"_index\":16,\"name\":{\"17\":{}},\"parent\":{}}],[\"interfaces\",{\"_index\":39,\"name\":{\"37\":{}},\"parent\":{}}],[\"isloggedin\",{\"_index\":20,\"name\":{\"21\":{}},\"parent\":{}}],[\"localstorage\",{\"_index\":49,\"name\":{\"47\":{}},\"parent\":{}}],[\"login\",{\"_index\":3,\"name\":{\"3\":{}},\"parent\":{}}],[\"loginpage\",{\"_index\":56,\"name\":{\"53\":{}},\"parent\":{}}],[\"name\",{\"_index\":14,\"name\":{\"13\":{},\"40\":{}},\"parent\":{}}],[\"navbar\",{\"_index\":9,\"name\":{\"8\":{}},\"parent\":{}}],[\"notfoundpage\",{\"_index\":54,\"name\":{\"52\":{}},\"parent\":{}}],[\"osocletters\",{\"_index\":10,\"name\":{\"9\":{}},\"parent\":{}}],[\"password\",{\"_index\":7,\"name\":{\"6\":{},\"14\":{}},\"parent\":{}}],[\"pendingpage\",{\"_index\":57,\"name\":{\"54\":{}},\"parent\":{}}],[\"privateroute\",{\"_index\":11,\"name\":{\"10\":{}},\"parent\":{}}],[\"projectspage\",{\"_index\":58,\"name\":{\"55\":{}},\"parent\":{}}],[\"register\",{\"_index\":12,\"name\":{\"11\":{}},\"parent\":{}}],[\"registerpage\",{\"_index\":59,\"name\":{\"56\":{}},\"parent\":{}}],[\"role\",{\"_index\":23,\"name\":{\"23\":{},\"34\":{}},\"parent\":{}}],[\"setbearertoken\",{\"_index\":48,\"name\":{\"46\":{}},\"parent\":{}}],[\"seteditions\",{\"_index\":28,\"name\":{\"28\":{}},\"parent\":{}}],[\"setisloggedin\",{\"_index\":22,\"name\":{\"22\":{}},\"parent\":{}}],[\"setrole\",{\"_index\":24,\"name\":{\"24\":{}},\"parent\":{}}],[\"settoken\",{\"_index\":26,\"name\":{\"26\":{},\"49\":{}},\"parent\":{}}],[\"socialbuttons\",{\"_index\":6,\"name\":{\"5\":{},\"16\":{}},\"parent\":{}}],[\"storagekey\",{\"_index\":32,\"name\":{\"32\":{}},\"parent\":{}}],[\"studentspage\",{\"_index\":60,\"name\":{\"57\":{}},\"parent\":{}}],[\"token\",{\"_index\":25,\"name\":{\"25\":{}},\"parent\":{}}],[\"user\",{\"_index\":40,\"name\":{\"38\":{}},\"parent\":{}}],[\"userid\",{\"_index\":42,\"name\":{\"39\":{}},\"parent\":{}}],[\"userspage\",{\"_index\":61,\"name\":{\"58\":{}},\"parent\":{}}],[\"utils\",{\"_index\":44,\"name\":{\"43\":{}},\"parent\":{\"44\":{},\"47\":{}}}],[\"utils.api\",{\"_index\":47,\"name\":{},\"parent\":{\"45\":{},\"46\":{}}}],[\"utils.localstorage\",{\"_index\":51,\"name\":{},\"parent\":{\"48\":{},\"49\":{}}}],[\"validateregistrationurl\",{\"_index\":46,\"name\":{\"45\":{}},\"parent\":{}}],[\"verifyingtokenpage\",{\"_index\":62,\"name\":{\"59\":{}},\"parent\":{}}],[\"views\",{\"_index\":52,\"name\":{\"50\":{}},\"parent\":{\"51\":{},\"53\":{},\"54\":{},\"55\":{},\"56\":{},\"57\":{},\"58\":{},\"59\":{}}}],[\"views.errors\",{\"_index\":55,\"name\":{},\"parent\":{\"52\":{}}}],[\"welcometext\",{\"_index\":4,\"name\":{\"4\":{}},\"parent\":{}}]],\"pipeline\":[]}}"); \ No newline at end of file diff --git a/frontend/docs/enums/Data.Enums.Role.html b/frontend/docs/enums/Data.Enums.Role.html index 89faed224..4094663a9 100644 --- a/frontend/docs/enums/Data.Enums.Role.html +++ b/frontend/docs/enums/Data.Enums.Role.html @@ -1,4 +1,4 @@ Role | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Enumeration Role

Enum for the different levels of authority a user can have

-

Index

Enumeration members

Enumeration members

ADMIN = 0
COACH = 1

Settings

Theme

Generated using TypeDoc

\ No newline at end of file +

Index

Enumeration members

Enumeration members

ADMIN = 0
COACH = 1

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/enums/Data.Enums.StorageKey.html b/frontend/docs/enums/Data.Enums.StorageKey.html index 45ef7ed20..97bcd8d88 100644 --- a/frontend/docs/enums/Data.Enums.StorageKey.html +++ b/frontend/docs/enums/Data.Enums.StorageKey.html @@ -1,3 +1,3 @@ StorageKey | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Enumeration StorageKey

Keys in LocalStorage

-

Index

Enumeration members

Enumeration members

BEARER_TOKEN = "bearerToken"

Settings

Theme

Generated using TypeDoc

\ No newline at end of file +

Index

Enumeration members

Enumeration members

BEARER_TOKEN = "bearerToken"

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/index.html b/frontend/docs/index.html index 6cdff7137..c858e1a64 100644 --- a/frontend/docs/index.html +++ b/frontend/docs/index.html @@ -104,4 +104,4 @@

yarn build

Builds the app for production to the build folder. It correctly bundles React in production mode and optimizes the build for the best performance.

The build is minified and the filenames include the hashes.

-

Settings

Theme

Generated using TypeDoc

\ No newline at end of file +

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/interfaces/Contexts.AuthContextState.html b/frontend/docs/interfaces/Contexts.AuthContextState.html index 75722106a..bcf39e00e 100644 --- a/frontend/docs/interfaces/Contexts.AuthContextState.html +++ b/frontend/docs/interfaces/Contexts.AuthContextState.html @@ -1 +1 @@ -AuthContextState | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface AuthContextState

Hierarchy

  • AuthContextState

Index

Properties

editions: number[]
isLoggedIn: null | boolean
role: null | Role
token: null | string

Methods

  • setEditions(value: number[]): void
  • setIsLoggedIn(value: null | boolean): void
  • setRole(value: null | Role): void
  • setToken(value: null | string): void

Legend

  • Property
  • Method

Settings

Theme

Generated using TypeDoc

\ No newline at end of file +AuthContextState | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface AuthContextState

Hierarchy

  • AuthContextState

Index

Properties

editions: number[]
isLoggedIn: null | boolean
role: null | Role
token: null | string

Methods

  • setEditions(value: number[]): void
  • setIsLoggedIn(value: null | boolean): void
  • setRole(value: null | Role): void
  • setToken(value: null | string): void

Legend

  • Property
  • Method

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/interfaces/Data.Interfaces.User.html b/frontend/docs/interfaces/Data.Interfaces.User.html index 1a91fa76e..cd05deb26 100644 --- a/frontend/docs/interfaces/Data.Interfaces.User.html +++ b/frontend/docs/interfaces/Data.Interfaces.User.html @@ -1 +1 @@ -User | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • User

Index

Properties

admin: boolean
editions: string[]
name: string
userId: number

Legend

  • Property

Settings

Theme

Generated using TypeDoc

\ No newline at end of file +User | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • User

Index

Properties

admin: boolean
editions: string[]
name: string
userId: number

Legend

  • Property

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules.html b/frontend/docs/modules.html index fee4bb2ff..017a347fc 100644 --- a/frontend/docs/modules.html +++ b/frontend/docs/modules.html @@ -1 +1 @@ -frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

frontend

Settings

Theme

Generated using TypeDoc

\ No newline at end of file +frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

frontend

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Components.Login.html b/frontend/docs/modules/Components.Login.html index b6fd1ef18..36b4158a2 100644 --- a/frontend/docs/modules/Components.Login.html +++ b/frontend/docs/modules/Components.Login.html @@ -1 +1 @@ -Login | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Functions

  • Email(__namedParameters: { email: string; setEmail: any }): Element
  • Password(__namedParameters: { password: string; callLogIn: any; setPassword: any }): Element
  • SocialButtons(): Element
  • WelcomeText(): Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file +Login | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Functions

  • Email(__namedParameters: { email: string; setEmail: any }): Element
  • Password(__namedParameters: { password: string; callLogIn: any; setPassword: any }): Element
  • SocialButtons(): Element
  • WelcomeText(): Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Components.Register.html b/frontend/docs/modules/Components.Register.html index fdcd6cb4a..74ace47ba 100644 --- a/frontend/docs/modules/Components.Register.html +++ b/frontend/docs/modules/Components.Register.html @@ -1 +1 @@ -Register | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Register

Index

Functions

  • BadInviteLink(): Element
  • ConfirmPassword(__namedParameters: { confirmPassword: string; callRegister: any; setConfirmPassword: any }): Element
  • Email(__namedParameters: { email: string; setEmail: any }): Element
  • InfoText(): Element
  • Name(__namedParameters: { name: string; setName: any }): Element
  • Password(__namedParameters: { password: string; setPassword: any }): Element
  • SocialButtons(): Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file +Register | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Register

Index

Functions

  • BadInviteLink(): Element
  • ConfirmPassword(__namedParameters: { confirmPassword: string; callRegister: any; setConfirmPassword: any }): Element
  • Email(__namedParameters: { email: string; setEmail: any }): Element
  • InfoText(): Element
  • Name(__namedParameters: { name: string; setName: any }): Element
  • Password(__namedParameters: { password: string; setPassword: any }): Element
  • SocialButtons(): Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Components.html b/frontend/docs/modules/Components.html index a2955d48b..097501732 100644 --- a/frontend/docs/modules/Components.html +++ b/frontend/docs/modules/Components.html @@ -1,7 +1,7 @@ -Components | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Components

Index

Functions

  • AdminRoute(): Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Contexts.html b/frontend/docs/modules/Contexts.html index a6a9b5228..9682d8a18 100644 --- a/frontend/docs/modules/Contexts.html +++ b/frontend/docs/modules/Contexts.html @@ -1,6 +1,6 @@ -Contexts | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Contexts

Index

Interfaces

Functions

Functions

  • AuthProvider(__namedParameters: { children: ReactNode }): Element
  • +Contexts | frontend
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Namespace Contexts

    Index

    Interfaces

    Functions

    Functions

    • AuthProvider(__namedParameters: { children: ReactNode }): Element
    • Provider for auth that creates getters, setters, maintains state, and provides default values

      Not strictly necessary but keeps the main App clean by handling this code here instead

      -

      Parameters

      • __namedParameters: { children: ReactNode }
        • children: ReactNode

      Returns Element

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +

    Parameters

    • __namedParameters: { children: ReactNode }
      • children: ReactNode

    Returns Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Data.Enums.html b/frontend/docs/modules/Data.Enums.html index 62742a807..7796028bb 100644 --- a/frontend/docs/modules/Data.Enums.html +++ b/frontend/docs/modules/Data.Enums.html @@ -1 +1 @@ -Enums | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Enums

Settings

Theme

Generated using TypeDoc

\ No newline at end of file +Enums | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Enums

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Data.Interfaces.html b/frontend/docs/modules/Data.Interfaces.html index 904b54260..eedd94daa 100644 --- a/frontend/docs/modules/Data.Interfaces.html +++ b/frontend/docs/modules/Data.Interfaces.html @@ -1 +1 @@ -Interfaces | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Interfaces

Settings

Theme

Generated using TypeDoc

\ No newline at end of file +Interfaces | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Interfaces

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Data.html b/frontend/docs/modules/Data.html index 138e0db44..870bc21f4 100644 --- a/frontend/docs/modules/Data.html +++ b/frontend/docs/modules/Data.html @@ -1 +1 @@ -Data | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Data

Settings

Theme

Generated using TypeDoc

\ No newline at end of file +Data | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Data

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Utils.Api.html b/frontend/docs/modules/Utils.Api.html new file mode 100644 index 000000000..630f3508d --- /dev/null +++ b/frontend/docs/modules/Utils.Api.html @@ -0,0 +1,6 @@ +Api | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Api

Index

Functions

  • setBearerToken(value: null | string): void
  • +

    Set the default bearer token in the request headers

    +

    Parameters

    • value: null | string

    Returns void

  • validateRegistrationUrl(edition: string, uuid: string): Promise<boolean>
  • +

    Check if a registration url exists by sending a GET to it, +if it returns a 200 then we know the url is valid.

    +

    Parameters

    • edition: string
    • uuid: string

    Returns Promise<boolean>

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Utils.LocalStorage.html b/frontend/docs/modules/Utils.LocalStorage.html new file mode 100644 index 000000000..0c1f6a38c --- /dev/null +++ b/frontend/docs/modules/Utils.LocalStorage.html @@ -0,0 +1,6 @@ +LocalStorage | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace LocalStorage

Index

Functions

  • getToken(): string | null
  • +

    Pull the user's token out of LocalStorage +Returns null if there is no token in LocalStorage yet

    +

    Returns string | null

  • setToken(value: string): void

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Utils.html b/frontend/docs/modules/Utils.html new file mode 100644 index 000000000..6f5cbf369 --- /dev/null +++ b/frontend/docs/modules/Utils.html @@ -0,0 +1 @@ +Utils | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Utils

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Views.Errors.html b/frontend/docs/modules/Views.Errors.html new file mode 100644 index 000000000..d6be5ee1a --- /dev/null +++ b/frontend/docs/modules/Views.Errors.html @@ -0,0 +1 @@ +Errors | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Errors

Index

Functions

Functions

  • NotFoundPage(): Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Views.html b/frontend/docs/modules/Views.html new file mode 100644 index 000000000..c6c985473 --- /dev/null +++ b/frontend/docs/modules/Views.html @@ -0,0 +1 @@ +Views | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Views

Index

Functions

  • LoginPage(): Element
  • PendingPage(): Element
  • ProjectsPage(): Element
  • RegisterPage(): Element
  • StudentsPage(): Element
  • UsersPage(): Element
  • VerifyingTokenPage(): Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/src/index.ts b/frontend/src/index.ts index e0af47457..36e681944 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -1,3 +1,5 @@ export * as Components from "./components"; export * as Contexts from "./contexts"; export * as Data from "./data"; +export * as Utils from "./utils"; +export * as Views from "./views"; diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts new file mode 100644 index 000000000..464de6bdf --- /dev/null +++ b/frontend/src/utils/index.ts @@ -0,0 +1,2 @@ +export * as Api from "./api"; +export * as LocalStorage from "./local-storage"; diff --git a/frontend/src/views/index.ts b/frontend/src/views/index.ts new file mode 100644 index 000000000..72c6efdc9 --- /dev/null +++ b/frontend/src/views/index.ts @@ -0,0 +1,8 @@ +export * as Errors from "./errors"; +export { default as LoginPage } from "./LoginPage"; +export { default as PendingPage } from "./PendingPage"; +export { default as ProjectsPage } from "./ProjectsPage"; +export { default as RegisterPage } from "./RegisterPage"; +export { default as StudentsPage } from "./StudentsPage"; +export { default as UsersPage } from "./UsersPage"; +export { default as VerifyingTokenPage } from "./VerifyingTokenPage"; From 0456ecfac98579136b26142521d265f92aea6abf Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 3 Apr 2022 00:36:10 +0200 Subject: [PATCH 214/536] Don't lint docs --- frontend/.eslintignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/.eslintignore b/frontend/.eslintignore index 6ebee9a67..2f78e518e 100644 --- a/frontend/.eslintignore +++ b/frontend/.eslintignore @@ -1,3 +1,4 @@ */.js node_modules -build \ No newline at end of file +build +docs \ No newline at end of file From c6d573c62dd697764e93e895789c3d5eb0faf6b0 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 3 Apr 2022 01:20:55 +0200 Subject: [PATCH 215/536] Add a couple of comments, fix breaking index file --- frontend/docs/assets/search.js | 2 +- frontend/docs/enums/Data.Enums.Role.html | 4 ---- frontend/docs/enums/Data.Enums.StorageKey.html | 3 --- frontend/docs/enums/data.Enums.Role.html | 4 ++++ frontend/docs/enums/data.Enums.StorageKey.html | 3 +++ frontend/docs/index.html | 4 ++-- .../docs/interfaces/Contexts.AuthContextState.html | 1 - frontend/docs/interfaces/Data.Interfaces.User.html | 1 - .../docs/interfaces/contexts.AuthContextState.html | 1 + frontend/docs/interfaces/data.Interfaces.User.html | 1 + frontend/docs/modules.html | 2 +- frontend/docs/modules/App.html | 1 + frontend/docs/modules/Components.Login.html | 1 - frontend/docs/modules/Components.Register.html | 1 - frontend/docs/modules/Components.html | 7 ------- frontend/docs/modules/Contexts.html | 6 ------ frontend/docs/modules/Data.Enums.html | 1 - frontend/docs/modules/Data.Interfaces.html | 1 - frontend/docs/modules/Data.html | 1 - frontend/docs/modules/Router.html | 1 + frontend/docs/modules/Utils.Api.html | 6 ------ frontend/docs/modules/Utils.LocalStorage.html | 6 ------ frontend/docs/modules/Utils.html | 1 - frontend/docs/modules/Views.Errors.html | 1 - frontend/docs/modules/Views.html | 1 - frontend/docs/modules/components.LoginComponents.html | 7 +++++++ .../docs/modules/components.RegisterComponents.html | 1 + frontend/docs/modules/components.html | 11 +++++++++++ frontend/docs/modules/contexts.html | 6 ++++++ frontend/docs/modules/data.Enums.html | 1 + frontend/docs/modules/data.Interfaces.html | 1 + frontend/docs/modules/data.html | 1 + frontend/docs/modules/utils.Api.html | 6 ++++++ frontend/docs/modules/utils.LocalStorage.html | 6 ++++++ frontend/docs/modules/utils.html | 1 + frontend/docs/modules/views.Errors.html | 1 + frontend/docs/modules/views.html | 1 + frontend/src/components/AdminRoute/AdminRoute.tsx | 4 ++-- frontend/src/components/Footer/Footer.tsx | 6 ++++++ .../LoginComponents/InputFields/Email/Email.tsx | 5 +++++ .../LoginComponents/InputFields/Password/Password.tsx | 6 ++++++ .../LoginComponents/SocialButtons/SocialButtons.tsx | 3 +++ frontend/src/components/index.ts | 4 ++-- frontend/src/index.ts | 5 ----- frontend/src/views/errors/index.ts | 1 + frontend/tsconfig.json | 9 ++++++++- 46 files changed, 91 insertions(+), 56 deletions(-) delete mode 100644 frontend/docs/enums/Data.Enums.Role.html delete mode 100644 frontend/docs/enums/Data.Enums.StorageKey.html create mode 100644 frontend/docs/enums/data.Enums.Role.html create mode 100644 frontend/docs/enums/data.Enums.StorageKey.html delete mode 100644 frontend/docs/interfaces/Contexts.AuthContextState.html delete mode 100644 frontend/docs/interfaces/Data.Interfaces.User.html create mode 100644 frontend/docs/interfaces/contexts.AuthContextState.html create mode 100644 frontend/docs/interfaces/data.Interfaces.User.html create mode 100644 frontend/docs/modules/App.html delete mode 100644 frontend/docs/modules/Components.Login.html delete mode 100644 frontend/docs/modules/Components.Register.html delete mode 100644 frontend/docs/modules/Components.html delete mode 100644 frontend/docs/modules/Contexts.html delete mode 100644 frontend/docs/modules/Data.Enums.html delete mode 100644 frontend/docs/modules/Data.Interfaces.html delete mode 100644 frontend/docs/modules/Data.html create mode 100644 frontend/docs/modules/Router.html delete mode 100644 frontend/docs/modules/Utils.Api.html delete mode 100644 frontend/docs/modules/Utils.LocalStorage.html delete mode 100644 frontend/docs/modules/Utils.html delete mode 100644 frontend/docs/modules/Views.Errors.html delete mode 100644 frontend/docs/modules/Views.html create mode 100644 frontend/docs/modules/components.LoginComponents.html create mode 100644 frontend/docs/modules/components.RegisterComponents.html create mode 100644 frontend/docs/modules/components.html create mode 100644 frontend/docs/modules/contexts.html create mode 100644 frontend/docs/modules/data.Enums.html create mode 100644 frontend/docs/modules/data.Interfaces.html create mode 100644 frontend/docs/modules/data.html create mode 100644 frontend/docs/modules/utils.Api.html create mode 100644 frontend/docs/modules/utils.LocalStorage.html create mode 100644 frontend/docs/modules/utils.html create mode 100644 frontend/docs/modules/views.Errors.html create mode 100644 frontend/docs/modules/views.html delete mode 100644 frontend/src/index.ts diff --git a/frontend/docs/assets/search.js b/frontend/docs/assets/search.js index 52dd6c057..3c0bcbf41 100644 --- a/frontend/docs/assets/search.js +++ b/frontend/docs/assets/search.js @@ -1 +1 @@ -window.searchData = JSON.parse("{\"kinds\":{\"4\":\"Namespace\",\"8\":\"Enumeration\",\"16\":\"Enumeration member\",\"64\":\"Function\",\"256\":\"Interface\",\"1024\":\"Property\",\"2048\":\"Method\"},\"rows\":[{\"id\":0,\"kind\":4,\"name\":\"Components\",\"url\":\"modules/Components.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":1,\"kind\":64,\"name\":\"AdminRoute\",\"url\":\"modules/Components.html#AdminRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":2,\"kind\":64,\"name\":\"Footer\",\"url\":\"modules/Components.html#Footer\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":3,\"kind\":4,\"name\":\"Login\",\"url\":\"modules/Components.Login.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":4,\"kind\":64,\"name\":\"WelcomeText\",\"url\":\"modules/Components.Login.html#WelcomeText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Login\"},{\"id\":5,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/Components.Login.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Login\"},{\"id\":6,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/Components.Login.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Login\"},{\"id\":7,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/Components.Login.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Login\"},{\"id\":8,\"kind\":64,\"name\":\"NavBar\",\"url\":\"modules/Components.html#NavBar\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":9,\"kind\":64,\"name\":\"OSOCLetters\",\"url\":\"modules/Components.html#OSOCLetters\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":10,\"kind\":64,\"name\":\"PrivateRoute\",\"url\":\"modules/Components.html#PrivateRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":11,\"kind\":4,\"name\":\"Register\",\"url\":\"modules/Components.Register.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Components\"},{\"id\":12,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/Components.Register.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":13,\"kind\":64,\"name\":\"Name\",\"url\":\"modules/Components.Register.html#Name\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":14,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/Components.Register.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":15,\"kind\":64,\"name\":\"ConfirmPassword\",\"url\":\"modules/Components.Register.html#ConfirmPassword\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":16,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/Components.Register.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":17,\"kind\":64,\"name\":\"InfoText\",\"url\":\"modules/Components.Register.html#InfoText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":18,\"kind\":64,\"name\":\"BadInviteLink\",\"url\":\"modules/Components.Register.html#BadInviteLink\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Components.Register\"},{\"id\":19,\"kind\":4,\"name\":\"Contexts\",\"url\":\"modules/Contexts.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":20,\"kind\":256,\"name\":\"AuthContextState\",\"url\":\"interfaces/Contexts.AuthContextState.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"Contexts\"},{\"id\":21,\"kind\":1024,\"name\":\"isLoggedIn\",\"url\":\"interfaces/Contexts.AuthContextState.html#isLoggedIn\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":22,\"kind\":2048,\"name\":\"setIsLoggedIn\",\"url\":\"interfaces/Contexts.AuthContextState.html#setIsLoggedIn\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":23,\"kind\":1024,\"name\":\"role\",\"url\":\"interfaces/Contexts.AuthContextState.html#role\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":24,\"kind\":2048,\"name\":\"setRole\",\"url\":\"interfaces/Contexts.AuthContextState.html#setRole\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":25,\"kind\":1024,\"name\":\"token\",\"url\":\"interfaces/Contexts.AuthContextState.html#token\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":26,\"kind\":2048,\"name\":\"setToken\",\"url\":\"interfaces/Contexts.AuthContextState.html#setToken\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":27,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/Contexts.AuthContextState.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":28,\"kind\":2048,\"name\":\"setEditions\",\"url\":\"interfaces/Contexts.AuthContextState.html#setEditions\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"Contexts.AuthContextState\"},{\"id\":29,\"kind\":64,\"name\":\"AuthProvider\",\"url\":\"modules/Contexts.html#AuthProvider\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Contexts\"},{\"id\":30,\"kind\":4,\"name\":\"Data\",\"url\":\"modules/Data.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":31,\"kind\":4,\"name\":\"Enums\",\"url\":\"modules/Data.Enums.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Data\"},{\"id\":32,\"kind\":8,\"name\":\"StorageKey\",\"url\":\"enums/Data.Enums.StorageKey.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"Data.Enums\"},{\"id\":33,\"kind\":16,\"name\":\"BEARER_TOKEN\",\"url\":\"enums/Data.Enums.StorageKey.html#BEARER_TOKEN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"Data.Enums.StorageKey\"},{\"id\":34,\"kind\":8,\"name\":\"Role\",\"url\":\"enums/Data.Enums.Role.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"Data.Enums\"},{\"id\":35,\"kind\":16,\"name\":\"ADMIN\",\"url\":\"enums/Data.Enums.Role.html#ADMIN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"Data.Enums.Role\"},{\"id\":36,\"kind\":16,\"name\":\"COACH\",\"url\":\"enums/Data.Enums.Role.html#COACH\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"Data.Enums.Role\"},{\"id\":37,\"kind\":4,\"name\":\"Interfaces\",\"url\":\"modules/Data.Interfaces.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Data\"},{\"id\":38,\"kind\":256,\"name\":\"User\",\"url\":\"interfaces/Data.Interfaces.User.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"Data.Interfaces\"},{\"id\":39,\"kind\":1024,\"name\":\"userId\",\"url\":\"interfaces/Data.Interfaces.User.html#userId\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Data.Interfaces.User\"},{\"id\":40,\"kind\":1024,\"name\":\"name\",\"url\":\"interfaces/Data.Interfaces.User.html#name\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Data.Interfaces.User\"},{\"id\":41,\"kind\":1024,\"name\":\"admin\",\"url\":\"interfaces/Data.Interfaces.User.html#admin\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Data.Interfaces.User\"},{\"id\":42,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/Data.Interfaces.User.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"Data.Interfaces.User\"},{\"id\":43,\"kind\":4,\"name\":\"Utils\",\"url\":\"modules/Utils.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":44,\"kind\":4,\"name\":\"Api\",\"url\":\"modules/Utils.Api.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Utils\"},{\"id\":45,\"kind\":64,\"name\":\"validateRegistrationUrl\",\"url\":\"modules/Utils.Api.html#validateRegistrationUrl\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Utils.Api\"},{\"id\":46,\"kind\":64,\"name\":\"setBearerToken\",\"url\":\"modules/Utils.Api.html#setBearerToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Utils.Api\"},{\"id\":47,\"kind\":4,\"name\":\"LocalStorage\",\"url\":\"modules/Utils.LocalStorage.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Utils\"},{\"id\":48,\"kind\":64,\"name\":\"getToken\",\"url\":\"modules/Utils.LocalStorage.html#getToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Utils.LocalStorage\"},{\"id\":49,\"kind\":64,\"name\":\"setToken\",\"url\":\"modules/Utils.LocalStorage.html#setToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Utils.LocalStorage\"},{\"id\":50,\"kind\":4,\"name\":\"Views\",\"url\":\"modules/Views.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":51,\"kind\":4,\"name\":\"Errors\",\"url\":\"modules/Views.Errors.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"Views\"},{\"id\":52,\"kind\":64,\"name\":\"NotFoundPage\",\"url\":\"modules/Views.Errors.html#NotFoundPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Views.Errors\"},{\"id\":53,\"kind\":64,\"name\":\"LoginPage\",\"url\":\"modules/Views.html#LoginPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Views\"},{\"id\":54,\"kind\":64,\"name\":\"PendingPage\",\"url\":\"modules/Views.html#PendingPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Views\"},{\"id\":55,\"kind\":64,\"name\":\"ProjectsPage\",\"url\":\"modules/Views.html#ProjectsPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Views\"},{\"id\":56,\"kind\":64,\"name\":\"RegisterPage\",\"url\":\"modules/Views.html#RegisterPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Views\"},{\"id\":57,\"kind\":64,\"name\":\"StudentsPage\",\"url\":\"modules/Views.html#StudentsPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Views\"},{\"id\":58,\"kind\":64,\"name\":\"UsersPage\",\"url\":\"modules/Views.html#UsersPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Views\"},{\"id\":59,\"kind\":64,\"name\":\"VerifyingTokenPage\",\"url\":\"modules/Views.html#VerifyingTokenPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"Views\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"parent\"],\"fieldVectors\":[[\"name/0\",[0,19.708]],[\"parent/0\",[]],[\"name/1\",[1,37.054]],[\"parent/1\",[0,1.9]],[\"name/2\",[2,37.054]],[\"parent/2\",[0,1.9]],[\"name/3\",[3,37.054]],[\"parent/3\",[0,1.9]],[\"name/4\",[4,37.054]],[\"parent/4\",[5,2.513]],[\"name/5\",[6,31.946]],[\"parent/5\",[5,2.513]],[\"name/6\",[7,31.946]],[\"parent/6\",[5,2.513]],[\"name/7\",[8,31.946]],[\"parent/7\",[5,2.513]],[\"name/8\",[9,37.054]],[\"parent/8\",[0,1.9]],[\"name/9\",[10,37.054]],[\"parent/9\",[0,1.9]],[\"name/10\",[11,37.054]],[\"parent/10\",[0,1.9]],[\"name/11\",[12,37.054]],[\"parent/11\",[0,1.9]],[\"name/12\",[8,31.946]],[\"parent/12\",[13,2.021]],[\"name/13\",[14,31.946]],[\"parent/13\",[13,2.021]],[\"name/14\",[7,31.946]],[\"parent/14\",[13,2.021]],[\"name/15\",[15,37.054]],[\"parent/15\",[13,2.021]],[\"name/16\",[6,31.946]],[\"parent/16\",[13,2.021]],[\"name/17\",[16,37.054]],[\"parent/17\",[13,2.021]],[\"name/18\",[17,37.054]],[\"parent/18\",[13,2.021]],[\"name/19\",[18,28.581]],[\"parent/19\",[]],[\"name/20\",[19,37.054]],[\"parent/20\",[18,2.756]],[\"name/21\",[20,37.054]],[\"parent/21\",[21,1.9]],[\"name/22\",[22,37.054]],[\"parent/22\",[21,1.9]],[\"name/23\",[23,31.946]],[\"parent/23\",[21,1.9]],[\"name/24\",[24,37.054]],[\"parent/24\",[21,1.9]],[\"name/25\",[25,37.054]],[\"parent/25\",[21,1.9]],[\"name/26\",[26,31.946]],[\"parent/26\",[21,1.9]],[\"name/27\",[27,31.946]],[\"parent/27\",[21,1.9]],[\"name/28\",[28,37.054]],[\"parent/28\",[21,1.9]],[\"name/29\",[29,37.054]],[\"parent/29\",[18,2.756]],[\"name/30\",[30,28.581]],[\"parent/30\",[]],[\"name/31\",[31,37.054]],[\"parent/31\",[30,2.756]],[\"name/32\",[32,37.054]],[\"parent/32\",[33,3.08]],[\"name/33\",[34,37.054]],[\"parent/33\",[35,3.573]],[\"name/34\",[23,31.946]],[\"parent/34\",[33,3.08]],[\"name/35\",[36,31.946]],[\"parent/35\",[37,3.08]],[\"name/36\",[38,37.054]],[\"parent/36\",[37,3.08]],[\"name/37\",[39,37.054]],[\"parent/37\",[30,2.756]],[\"name/38\",[40,37.054]],[\"parent/38\",[41,3.573]],[\"name/39\",[42,37.054]],[\"parent/39\",[43,2.513]],[\"name/40\",[14,31.946]],[\"parent/40\",[43,2.513]],[\"name/41\",[36,31.946]],[\"parent/41\",[43,2.513]],[\"name/42\",[27,31.946]],[\"parent/42\",[43,2.513]],[\"name/43\",[44,28.581]],[\"parent/43\",[]],[\"name/44\",[45,37.054]],[\"parent/44\",[44,2.756]],[\"name/45\",[46,37.054]],[\"parent/45\",[47,3.08]],[\"name/46\",[48,37.054]],[\"parent/46\",[47,3.08]],[\"name/47\",[49,37.054]],[\"parent/47\",[44,2.756]],[\"name/48\",[50,37.054]],[\"parent/48\",[51,3.08]],[\"name/49\",[26,31.946]],[\"parent/49\",[51,3.08]],[\"name/50\",[52,18.596]],[\"parent/50\",[]],[\"name/51\",[53,37.054]],[\"parent/51\",[52,1.793]],[\"name/52\",[54,37.054]],[\"parent/52\",[55,3.573]],[\"name/53\",[56,37.054]],[\"parent/53\",[52,1.793]],[\"name/54\",[57,37.054]],[\"parent/54\",[52,1.793]],[\"name/55\",[58,37.054]],[\"parent/55\",[52,1.793]],[\"name/56\",[59,37.054]],[\"parent/56\",[52,1.793]],[\"name/57\",[60,37.054]],[\"parent/57\",[52,1.793]],[\"name/58\",[61,37.054]],[\"parent/58\",[52,1.793]],[\"name/59\",[62,37.054]],[\"parent/59\",[52,1.793]]],\"invertedIndex\":[[\"admin\",{\"_index\":36,\"name\":{\"35\":{},\"41\":{}},\"parent\":{}}],[\"adminroute\",{\"_index\":1,\"name\":{\"1\":{}},\"parent\":{}}],[\"api\",{\"_index\":45,\"name\":{\"44\":{}},\"parent\":{}}],[\"authcontextstate\",{\"_index\":19,\"name\":{\"20\":{}},\"parent\":{}}],[\"authprovider\",{\"_index\":29,\"name\":{\"29\":{}},\"parent\":{}}],[\"badinvitelink\",{\"_index\":17,\"name\":{\"18\":{}},\"parent\":{}}],[\"bearer_token\",{\"_index\":34,\"name\":{\"33\":{}},\"parent\":{}}],[\"coach\",{\"_index\":38,\"name\":{\"36\":{}},\"parent\":{}}],[\"components\",{\"_index\":0,\"name\":{\"0\":{}},\"parent\":{\"1\":{},\"2\":{},\"3\":{},\"8\":{},\"9\":{},\"10\":{},\"11\":{}}}],[\"components.login\",{\"_index\":5,\"name\":{},\"parent\":{\"4\":{},\"5\":{},\"6\":{},\"7\":{}}}],[\"components.register\",{\"_index\":13,\"name\":{},\"parent\":{\"12\":{},\"13\":{},\"14\":{},\"15\":{},\"16\":{},\"17\":{},\"18\":{}}}],[\"confirmpassword\",{\"_index\":15,\"name\":{\"15\":{}},\"parent\":{}}],[\"contexts\",{\"_index\":18,\"name\":{\"19\":{}},\"parent\":{\"20\":{},\"29\":{}}}],[\"contexts.authcontextstate\",{\"_index\":21,\"name\":{},\"parent\":{\"21\":{},\"22\":{},\"23\":{},\"24\":{},\"25\":{},\"26\":{},\"27\":{},\"28\":{}}}],[\"data\",{\"_index\":30,\"name\":{\"30\":{}},\"parent\":{\"31\":{},\"37\":{}}}],[\"data.enums\",{\"_index\":33,\"name\":{},\"parent\":{\"32\":{},\"34\":{}}}],[\"data.enums.role\",{\"_index\":37,\"name\":{},\"parent\":{\"35\":{},\"36\":{}}}],[\"data.enums.storagekey\",{\"_index\":35,\"name\":{},\"parent\":{\"33\":{}}}],[\"data.interfaces\",{\"_index\":41,\"name\":{},\"parent\":{\"38\":{}}}],[\"data.interfaces.user\",{\"_index\":43,\"name\":{},\"parent\":{\"39\":{},\"40\":{},\"41\":{},\"42\":{}}}],[\"editions\",{\"_index\":27,\"name\":{\"27\":{},\"42\":{}},\"parent\":{}}],[\"email\",{\"_index\":8,\"name\":{\"7\":{},\"12\":{}},\"parent\":{}}],[\"enums\",{\"_index\":31,\"name\":{\"31\":{}},\"parent\":{}}],[\"errors\",{\"_index\":53,\"name\":{\"51\":{}},\"parent\":{}}],[\"footer\",{\"_index\":2,\"name\":{\"2\":{}},\"parent\":{}}],[\"gettoken\",{\"_index\":50,\"name\":{\"48\":{}},\"parent\":{}}],[\"infotext\",{\"_index\":16,\"name\":{\"17\":{}},\"parent\":{}}],[\"interfaces\",{\"_index\":39,\"name\":{\"37\":{}},\"parent\":{}}],[\"isloggedin\",{\"_index\":20,\"name\":{\"21\":{}},\"parent\":{}}],[\"localstorage\",{\"_index\":49,\"name\":{\"47\":{}},\"parent\":{}}],[\"login\",{\"_index\":3,\"name\":{\"3\":{}},\"parent\":{}}],[\"loginpage\",{\"_index\":56,\"name\":{\"53\":{}},\"parent\":{}}],[\"name\",{\"_index\":14,\"name\":{\"13\":{},\"40\":{}},\"parent\":{}}],[\"navbar\",{\"_index\":9,\"name\":{\"8\":{}},\"parent\":{}}],[\"notfoundpage\",{\"_index\":54,\"name\":{\"52\":{}},\"parent\":{}}],[\"osocletters\",{\"_index\":10,\"name\":{\"9\":{}},\"parent\":{}}],[\"password\",{\"_index\":7,\"name\":{\"6\":{},\"14\":{}},\"parent\":{}}],[\"pendingpage\",{\"_index\":57,\"name\":{\"54\":{}},\"parent\":{}}],[\"privateroute\",{\"_index\":11,\"name\":{\"10\":{}},\"parent\":{}}],[\"projectspage\",{\"_index\":58,\"name\":{\"55\":{}},\"parent\":{}}],[\"register\",{\"_index\":12,\"name\":{\"11\":{}},\"parent\":{}}],[\"registerpage\",{\"_index\":59,\"name\":{\"56\":{}},\"parent\":{}}],[\"role\",{\"_index\":23,\"name\":{\"23\":{},\"34\":{}},\"parent\":{}}],[\"setbearertoken\",{\"_index\":48,\"name\":{\"46\":{}},\"parent\":{}}],[\"seteditions\",{\"_index\":28,\"name\":{\"28\":{}},\"parent\":{}}],[\"setisloggedin\",{\"_index\":22,\"name\":{\"22\":{}},\"parent\":{}}],[\"setrole\",{\"_index\":24,\"name\":{\"24\":{}},\"parent\":{}}],[\"settoken\",{\"_index\":26,\"name\":{\"26\":{},\"49\":{}},\"parent\":{}}],[\"socialbuttons\",{\"_index\":6,\"name\":{\"5\":{},\"16\":{}},\"parent\":{}}],[\"storagekey\",{\"_index\":32,\"name\":{\"32\":{}},\"parent\":{}}],[\"studentspage\",{\"_index\":60,\"name\":{\"57\":{}},\"parent\":{}}],[\"token\",{\"_index\":25,\"name\":{\"25\":{}},\"parent\":{}}],[\"user\",{\"_index\":40,\"name\":{\"38\":{}},\"parent\":{}}],[\"userid\",{\"_index\":42,\"name\":{\"39\":{}},\"parent\":{}}],[\"userspage\",{\"_index\":61,\"name\":{\"58\":{}},\"parent\":{}}],[\"utils\",{\"_index\":44,\"name\":{\"43\":{}},\"parent\":{\"44\":{},\"47\":{}}}],[\"utils.api\",{\"_index\":47,\"name\":{},\"parent\":{\"45\":{},\"46\":{}}}],[\"utils.localstorage\",{\"_index\":51,\"name\":{},\"parent\":{\"48\":{},\"49\":{}}}],[\"validateregistrationurl\",{\"_index\":46,\"name\":{\"45\":{}},\"parent\":{}}],[\"verifyingtokenpage\",{\"_index\":62,\"name\":{\"59\":{}},\"parent\":{}}],[\"views\",{\"_index\":52,\"name\":{\"50\":{}},\"parent\":{\"51\":{},\"53\":{},\"54\":{},\"55\":{},\"56\":{},\"57\":{},\"58\":{},\"59\":{}}}],[\"views.errors\",{\"_index\":55,\"name\":{},\"parent\":{\"52\":{}}}],[\"welcometext\",{\"_index\":4,\"name\":{\"4\":{}},\"parent\":{}}]],\"pipeline\":[]}}"); \ No newline at end of file +window.searchData = JSON.parse("{\"kinds\":{\"2\":\"Module\",\"4\":\"Namespace\",\"8\":\"Enumeration\",\"16\":\"Enumeration member\",\"64\":\"Function\",\"256\":\"Interface\",\"1024\":\"Property\",\"2048\":\"Method\"},\"rows\":[{\"id\":0,\"kind\":2,\"name\":\"App\",\"url\":\"modules/App.html\",\"classes\":\"tsd-kind-module\"},{\"id\":1,\"kind\":64,\"name\":\"default\",\"url\":\"modules/App.html#default\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"App\"},{\"id\":2,\"kind\":2,\"name\":\"Router\",\"url\":\"modules/Router.html\",\"classes\":\"tsd-kind-module\"},{\"id\":3,\"kind\":64,\"name\":\"default\",\"url\":\"modules/Router.html#default\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"Router\"},{\"id\":4,\"kind\":2,\"name\":\"components\",\"url\":\"modules/components.html\",\"classes\":\"tsd-kind-module\"},{\"id\":5,\"kind\":2,\"name\":\"contexts\",\"url\":\"modules/contexts.html\",\"classes\":\"tsd-kind-module\"},{\"id\":6,\"kind\":2,\"name\":\"data\",\"url\":\"modules/data.html\",\"classes\":\"tsd-kind-module\"},{\"id\":7,\"kind\":2,\"name\":\"utils\",\"url\":\"modules/utils.html\",\"classes\":\"tsd-kind-module\"},{\"id\":8,\"kind\":2,\"name\":\"views\",\"url\":\"modules/views.html\",\"classes\":\"tsd-kind-module\"},{\"id\":9,\"kind\":64,\"name\":\"AdminRoute\",\"url\":\"modules/components.html#AdminRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":10,\"kind\":64,\"name\":\"Footer\",\"url\":\"modules/components.html#Footer\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":11,\"kind\":4,\"name\":\"LoginComponents\",\"url\":\"modules/components.LoginComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":12,\"kind\":64,\"name\":\"WelcomeText\",\"url\":\"modules/components.LoginComponents.html#WelcomeText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":13,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/components.LoginComponents.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":14,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/components.LoginComponents.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":15,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/components.LoginComponents.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":16,\"kind\":64,\"name\":\"NavBar\",\"url\":\"modules/components.html#NavBar\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":17,\"kind\":64,\"name\":\"OSOCLetters\",\"url\":\"modules/components.html#OSOCLetters\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":18,\"kind\":64,\"name\":\"PrivateRoute\",\"url\":\"modules/components.html#PrivateRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":19,\"kind\":4,\"name\":\"RegisterComponents\",\"url\":\"modules/components.RegisterComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":20,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/components.RegisterComponents.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":21,\"kind\":64,\"name\":\"Name\",\"url\":\"modules/components.RegisterComponents.html#Name\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":22,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/components.RegisterComponents.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":23,\"kind\":64,\"name\":\"ConfirmPassword\",\"url\":\"modules/components.RegisterComponents.html#ConfirmPassword\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":24,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/components.RegisterComponents.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":25,\"kind\":64,\"name\":\"InfoText\",\"url\":\"modules/components.RegisterComponents.html#InfoText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":26,\"kind\":64,\"name\":\"BadInviteLink\",\"url\":\"modules/components.RegisterComponents.html#BadInviteLink\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":27,\"kind\":256,\"name\":\"AuthContextState\",\"url\":\"interfaces/contexts.AuthContextState.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-module\",\"parent\":\"contexts\"},{\"id\":28,\"kind\":1024,\"name\":\"isLoggedIn\",\"url\":\"interfaces/contexts.AuthContextState.html#isLoggedIn\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":29,\"kind\":2048,\"name\":\"setIsLoggedIn\",\"url\":\"interfaces/contexts.AuthContextState.html#setIsLoggedIn\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":30,\"kind\":1024,\"name\":\"role\",\"url\":\"interfaces/contexts.AuthContextState.html#role\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":31,\"kind\":2048,\"name\":\"setRole\",\"url\":\"interfaces/contexts.AuthContextState.html#setRole\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":32,\"kind\":1024,\"name\":\"token\",\"url\":\"interfaces/contexts.AuthContextState.html#token\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":33,\"kind\":2048,\"name\":\"setToken\",\"url\":\"interfaces/contexts.AuthContextState.html#setToken\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":34,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/contexts.AuthContextState.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":35,\"kind\":2048,\"name\":\"setEditions\",\"url\":\"interfaces/contexts.AuthContextState.html#setEditions\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":36,\"kind\":64,\"name\":\"AuthProvider\",\"url\":\"modules/contexts.html#AuthProvider\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"contexts\"},{\"id\":37,\"kind\":4,\"name\":\"Enums\",\"url\":\"modules/data.Enums.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"data\"},{\"id\":38,\"kind\":8,\"name\":\"StorageKey\",\"url\":\"enums/data.Enums.StorageKey.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"data.Enums\"},{\"id\":39,\"kind\":16,\"name\":\"BEARER_TOKEN\",\"url\":\"enums/data.Enums.StorageKey.html#BEARER_TOKEN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.StorageKey\"},{\"id\":40,\"kind\":8,\"name\":\"Role\",\"url\":\"enums/data.Enums.Role.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"data.Enums\"},{\"id\":41,\"kind\":16,\"name\":\"ADMIN\",\"url\":\"enums/data.Enums.Role.html#ADMIN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.Role\"},{\"id\":42,\"kind\":16,\"name\":\"COACH\",\"url\":\"enums/data.Enums.Role.html#COACH\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.Role\"},{\"id\":43,\"kind\":4,\"name\":\"Interfaces\",\"url\":\"modules/data.Interfaces.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"data\"},{\"id\":44,\"kind\":256,\"name\":\"User\",\"url\":\"interfaces/data.Interfaces.User.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"data.Interfaces\"},{\"id\":45,\"kind\":1024,\"name\":\"userId\",\"url\":\"interfaces/data.Interfaces.User.html#userId\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":46,\"kind\":1024,\"name\":\"name\",\"url\":\"interfaces/data.Interfaces.User.html#name\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":47,\"kind\":1024,\"name\":\"admin\",\"url\":\"interfaces/data.Interfaces.User.html#admin\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":48,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/data.Interfaces.User.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":49,\"kind\":4,\"name\":\"Api\",\"url\":\"modules/utils.Api.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"utils\"},{\"id\":50,\"kind\":64,\"name\":\"validateRegistrationUrl\",\"url\":\"modules/utils.Api.html#validateRegistrationUrl\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api\"},{\"id\":51,\"kind\":64,\"name\":\"setBearerToken\",\"url\":\"modules/utils.Api.html#setBearerToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api\"},{\"id\":52,\"kind\":4,\"name\":\"LocalStorage\",\"url\":\"modules/utils.LocalStorage.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"utils\"},{\"id\":53,\"kind\":64,\"name\":\"getToken\",\"url\":\"modules/utils.LocalStorage.html#getToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.LocalStorage\"},{\"id\":54,\"kind\":64,\"name\":\"setToken\",\"url\":\"modules/utils.LocalStorage.html#setToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.LocalStorage\"},{\"id\":55,\"kind\":4,\"name\":\"Errors\",\"url\":\"modules/views.Errors.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":56,\"kind\":64,\"name\":\"ForbiddenPage\",\"url\":\"modules/views.Errors.html#ForbiddenPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"views.Errors\"},{\"id\":57,\"kind\":64,\"name\":\"NotFoundPage\",\"url\":\"modules/views.Errors.html#NotFoundPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"views.Errors\"},{\"id\":58,\"kind\":64,\"name\":\"LoginPage\",\"url\":\"modules/views.html#LoginPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":59,\"kind\":64,\"name\":\"PendingPage\",\"url\":\"modules/views.html#PendingPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":60,\"kind\":64,\"name\":\"ProjectsPage\",\"url\":\"modules/views.html#ProjectsPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":61,\"kind\":64,\"name\":\"RegisterPage\",\"url\":\"modules/views.html#RegisterPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":62,\"kind\":64,\"name\":\"StudentsPage\",\"url\":\"modules/views.html#StudentsPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":63,\"kind\":64,\"name\":\"UsersPage\",\"url\":\"modules/views.html#UsersPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":64,\"kind\":64,\"name\":\"VerifyingTokenPage\",\"url\":\"modules/views.html#VerifyingTokenPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"parent\"],\"fieldVectors\":[[\"name/0\",[0,32.734]],[\"parent/0\",[]],[\"name/1\",[1,32.734]],[\"parent/1\",[0,3.119]],[\"name/2\",[2,32.734]],[\"parent/2\",[]],[\"name/3\",[1,32.734]],[\"parent/3\",[2,3.119]],[\"name/4\",[3,20.496]],[\"parent/4\",[]],[\"name/5\",[4,29.369]],[\"parent/5\",[]],[\"name/6\",[5,29.369]],[\"parent/6\",[]],[\"name/7\",[6,29.369]],[\"parent/7\",[]],[\"name/8\",[7,19.384]],[\"parent/8\",[]],[\"name/9\",[8,37.842]],[\"parent/9\",[3,1.953]],[\"name/10\",[9,37.842]],[\"parent/10\",[3,1.953]],[\"name/11\",[10,37.842]],[\"parent/11\",[3,1.953]],[\"name/12\",[11,37.842]],[\"parent/12\",[12,2.559]],[\"name/13\",[13,32.734]],[\"parent/13\",[12,2.559]],[\"name/14\",[14,32.734]],[\"parent/14\",[12,2.559]],[\"name/15\",[15,32.734]],[\"parent/15\",[12,2.559]],[\"name/16\",[16,37.842]],[\"parent/16\",[3,1.953]],[\"name/17\",[17,37.842]],[\"parent/17\",[3,1.953]],[\"name/18\",[18,37.842]],[\"parent/18\",[3,1.953]],[\"name/19\",[19,37.842]],[\"parent/19\",[3,1.953]],[\"name/20\",[15,32.734]],[\"parent/20\",[20,2.072]],[\"name/21\",[21,32.734]],[\"parent/21\",[20,2.072]],[\"name/22\",[14,32.734]],[\"parent/22\",[20,2.072]],[\"name/23\",[22,37.842]],[\"parent/23\",[20,2.072]],[\"name/24\",[13,32.734]],[\"parent/24\",[20,2.072]],[\"name/25\",[23,37.842]],[\"parent/25\",[20,2.072]],[\"name/26\",[24,37.842]],[\"parent/26\",[20,2.072]],[\"name/27\",[25,37.842]],[\"parent/27\",[4,2.799]],[\"name/28\",[26,37.842]],[\"parent/28\",[27,1.953]],[\"name/29\",[28,37.842]],[\"parent/29\",[27,1.953]],[\"name/30\",[29,32.734]],[\"parent/30\",[27,1.953]],[\"name/31\",[30,37.842]],[\"parent/31\",[27,1.953]],[\"name/32\",[31,37.842]],[\"parent/32\",[27,1.953]],[\"name/33\",[32,32.734]],[\"parent/33\",[27,1.953]],[\"name/34\",[33,32.734]],[\"parent/34\",[27,1.953]],[\"name/35\",[34,37.842]],[\"parent/35\",[27,1.953]],[\"name/36\",[35,37.842]],[\"parent/36\",[4,2.799]],[\"name/37\",[36,37.842]],[\"parent/37\",[5,2.799]],[\"name/38\",[37,37.842]],[\"parent/38\",[38,3.119]],[\"name/39\",[39,37.842]],[\"parent/39\",[40,3.606]],[\"name/40\",[29,32.734]],[\"parent/40\",[38,3.119]],[\"name/41\",[41,32.734]],[\"parent/41\",[42,3.119]],[\"name/42\",[43,37.842]],[\"parent/42\",[42,3.119]],[\"name/43\",[44,37.842]],[\"parent/43\",[5,2.799]],[\"name/44\",[45,37.842]],[\"parent/44\",[46,3.606]],[\"name/45\",[47,37.842]],[\"parent/45\",[48,2.559]],[\"name/46\",[21,32.734]],[\"parent/46\",[48,2.559]],[\"name/47\",[41,32.734]],[\"parent/47\",[48,2.559]],[\"name/48\",[33,32.734]],[\"parent/48\",[48,2.559]],[\"name/49\",[49,37.842]],[\"parent/49\",[6,2.799]],[\"name/50\",[50,37.842]],[\"parent/50\",[51,3.119]],[\"name/51\",[52,37.842]],[\"parent/51\",[51,3.119]],[\"name/52\",[53,37.842]],[\"parent/52\",[6,2.799]],[\"name/53\",[54,37.842]],[\"parent/53\",[55,3.119]],[\"name/54\",[32,32.734]],[\"parent/54\",[55,3.119]],[\"name/55\",[56,37.842]],[\"parent/55\",[7,1.847]],[\"name/56\",[57,37.842]],[\"parent/56\",[58,3.119]],[\"name/57\",[59,37.842]],[\"parent/57\",[58,3.119]],[\"name/58\",[60,37.842]],[\"parent/58\",[7,1.847]],[\"name/59\",[61,37.842]],[\"parent/59\",[7,1.847]],[\"name/60\",[62,37.842]],[\"parent/60\",[7,1.847]],[\"name/61\",[63,37.842]],[\"parent/61\",[7,1.847]],[\"name/62\",[64,37.842]],[\"parent/62\",[7,1.847]],[\"name/63\",[65,37.842]],[\"parent/63\",[7,1.847]],[\"name/64\",[66,37.842]],[\"parent/64\",[7,1.847]]],\"invertedIndex\":[[\"admin\",{\"_index\":41,\"name\":{\"41\":{},\"47\":{}},\"parent\":{}}],[\"adminroute\",{\"_index\":8,\"name\":{\"9\":{}},\"parent\":{}}],[\"api\",{\"_index\":49,\"name\":{\"49\":{}},\"parent\":{}}],[\"app\",{\"_index\":0,\"name\":{\"0\":{}},\"parent\":{\"1\":{}}}],[\"authcontextstate\",{\"_index\":25,\"name\":{\"27\":{}},\"parent\":{}}],[\"authprovider\",{\"_index\":35,\"name\":{\"36\":{}},\"parent\":{}}],[\"badinvitelink\",{\"_index\":24,\"name\":{\"26\":{}},\"parent\":{}}],[\"bearer_token\",{\"_index\":39,\"name\":{\"39\":{}},\"parent\":{}}],[\"coach\",{\"_index\":43,\"name\":{\"42\":{}},\"parent\":{}}],[\"components\",{\"_index\":3,\"name\":{\"4\":{}},\"parent\":{\"9\":{},\"10\":{},\"11\":{},\"16\":{},\"17\":{},\"18\":{},\"19\":{}}}],[\"components.logincomponents\",{\"_index\":12,\"name\":{},\"parent\":{\"12\":{},\"13\":{},\"14\":{},\"15\":{}}}],[\"components.registercomponents\",{\"_index\":20,\"name\":{},\"parent\":{\"20\":{},\"21\":{},\"22\":{},\"23\":{},\"24\":{},\"25\":{},\"26\":{}}}],[\"confirmpassword\",{\"_index\":22,\"name\":{\"23\":{}},\"parent\":{}}],[\"contexts\",{\"_index\":4,\"name\":{\"5\":{}},\"parent\":{\"27\":{},\"36\":{}}}],[\"contexts.authcontextstate\",{\"_index\":27,\"name\":{},\"parent\":{\"28\":{},\"29\":{},\"30\":{},\"31\":{},\"32\":{},\"33\":{},\"34\":{},\"35\":{}}}],[\"data\",{\"_index\":5,\"name\":{\"6\":{}},\"parent\":{\"37\":{},\"43\":{}}}],[\"data.enums\",{\"_index\":38,\"name\":{},\"parent\":{\"38\":{},\"40\":{}}}],[\"data.enums.role\",{\"_index\":42,\"name\":{},\"parent\":{\"41\":{},\"42\":{}}}],[\"data.enums.storagekey\",{\"_index\":40,\"name\":{},\"parent\":{\"39\":{}}}],[\"data.interfaces\",{\"_index\":46,\"name\":{},\"parent\":{\"44\":{}}}],[\"data.interfaces.user\",{\"_index\":48,\"name\":{},\"parent\":{\"45\":{},\"46\":{},\"47\":{},\"48\":{}}}],[\"default\",{\"_index\":1,\"name\":{\"1\":{},\"3\":{}},\"parent\":{}}],[\"editions\",{\"_index\":33,\"name\":{\"34\":{},\"48\":{}},\"parent\":{}}],[\"email\",{\"_index\":15,\"name\":{\"15\":{},\"20\":{}},\"parent\":{}}],[\"enums\",{\"_index\":36,\"name\":{\"37\":{}},\"parent\":{}}],[\"errors\",{\"_index\":56,\"name\":{\"55\":{}},\"parent\":{}}],[\"footer\",{\"_index\":9,\"name\":{\"10\":{}},\"parent\":{}}],[\"forbiddenpage\",{\"_index\":57,\"name\":{\"56\":{}},\"parent\":{}}],[\"gettoken\",{\"_index\":54,\"name\":{\"53\":{}},\"parent\":{}}],[\"infotext\",{\"_index\":23,\"name\":{\"25\":{}},\"parent\":{}}],[\"interfaces\",{\"_index\":44,\"name\":{\"43\":{}},\"parent\":{}}],[\"isloggedin\",{\"_index\":26,\"name\":{\"28\":{}},\"parent\":{}}],[\"localstorage\",{\"_index\":53,\"name\":{\"52\":{}},\"parent\":{}}],[\"logincomponents\",{\"_index\":10,\"name\":{\"11\":{}},\"parent\":{}}],[\"loginpage\",{\"_index\":60,\"name\":{\"58\":{}},\"parent\":{}}],[\"name\",{\"_index\":21,\"name\":{\"21\":{},\"46\":{}},\"parent\":{}}],[\"navbar\",{\"_index\":16,\"name\":{\"16\":{}},\"parent\":{}}],[\"notfoundpage\",{\"_index\":59,\"name\":{\"57\":{}},\"parent\":{}}],[\"osocletters\",{\"_index\":17,\"name\":{\"17\":{}},\"parent\":{}}],[\"password\",{\"_index\":14,\"name\":{\"14\":{},\"22\":{}},\"parent\":{}}],[\"pendingpage\",{\"_index\":61,\"name\":{\"59\":{}},\"parent\":{}}],[\"privateroute\",{\"_index\":18,\"name\":{\"18\":{}},\"parent\":{}}],[\"projectspage\",{\"_index\":62,\"name\":{\"60\":{}},\"parent\":{}}],[\"registercomponents\",{\"_index\":19,\"name\":{\"19\":{}},\"parent\":{}}],[\"registerpage\",{\"_index\":63,\"name\":{\"61\":{}},\"parent\":{}}],[\"role\",{\"_index\":29,\"name\":{\"30\":{},\"40\":{}},\"parent\":{}}],[\"router\",{\"_index\":2,\"name\":{\"2\":{}},\"parent\":{\"3\":{}}}],[\"setbearertoken\",{\"_index\":52,\"name\":{\"51\":{}},\"parent\":{}}],[\"seteditions\",{\"_index\":34,\"name\":{\"35\":{}},\"parent\":{}}],[\"setisloggedin\",{\"_index\":28,\"name\":{\"29\":{}},\"parent\":{}}],[\"setrole\",{\"_index\":30,\"name\":{\"31\":{}},\"parent\":{}}],[\"settoken\",{\"_index\":32,\"name\":{\"33\":{},\"54\":{}},\"parent\":{}}],[\"socialbuttons\",{\"_index\":13,\"name\":{\"13\":{},\"24\":{}},\"parent\":{}}],[\"storagekey\",{\"_index\":37,\"name\":{\"38\":{}},\"parent\":{}}],[\"studentspage\",{\"_index\":64,\"name\":{\"62\":{}},\"parent\":{}}],[\"token\",{\"_index\":31,\"name\":{\"32\":{}},\"parent\":{}}],[\"user\",{\"_index\":45,\"name\":{\"44\":{}},\"parent\":{}}],[\"userid\",{\"_index\":47,\"name\":{\"45\":{}},\"parent\":{}}],[\"userspage\",{\"_index\":65,\"name\":{\"63\":{}},\"parent\":{}}],[\"utils\",{\"_index\":6,\"name\":{\"7\":{}},\"parent\":{\"49\":{},\"52\":{}}}],[\"utils.api\",{\"_index\":51,\"name\":{},\"parent\":{\"50\":{},\"51\":{}}}],[\"utils.localstorage\",{\"_index\":55,\"name\":{},\"parent\":{\"53\":{},\"54\":{}}}],[\"validateregistrationurl\",{\"_index\":50,\"name\":{\"50\":{}},\"parent\":{}}],[\"verifyingtokenpage\",{\"_index\":66,\"name\":{\"64\":{}},\"parent\":{}}],[\"views\",{\"_index\":7,\"name\":{\"8\":{}},\"parent\":{\"55\":{},\"58\":{},\"59\":{},\"60\":{},\"61\":{},\"62\":{},\"63\":{},\"64\":{}}}],[\"views.errors\",{\"_index\":58,\"name\":{},\"parent\":{\"56\":{},\"57\":{}}}],[\"welcometext\",{\"_index\":11,\"name\":{\"12\":{}},\"parent\":{}}]],\"pipeline\":[]}}"); \ No newline at end of file diff --git a/frontend/docs/enums/Data.Enums.Role.html b/frontend/docs/enums/Data.Enums.Role.html deleted file mode 100644 index 4094663a9..000000000 --- a/frontend/docs/enums/Data.Enums.Role.html +++ /dev/null @@ -1,4 +0,0 @@ -Role | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Enumeration Role

-

Enum for the different levels of authority a user -can have

-

Index

Enumeration members

Enumeration members

ADMIN = 0
COACH = 1

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/enums/Data.Enums.StorageKey.html b/frontend/docs/enums/Data.Enums.StorageKey.html deleted file mode 100644 index 97bcd8d88..000000000 --- a/frontend/docs/enums/Data.Enums.StorageKey.html +++ /dev/null @@ -1,3 +0,0 @@ -StorageKey | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Enumeration StorageKey

-

Keys in LocalStorage

-

Index

Enumeration members

Enumeration members

BEARER_TOKEN = "bearerToken"

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/enums/data.Enums.Role.html b/frontend/docs/enums/data.Enums.Role.html new file mode 100644 index 000000000..b4381e97f --- /dev/null +++ b/frontend/docs/enums/data.Enums.Role.html @@ -0,0 +1,4 @@ +Role | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu
+

Enum for the different levels of authority a user +can have

+

Index

Enumeration members

Enumeration members

ADMIN = 0
COACH = 1

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/enums/data.Enums.StorageKey.html b/frontend/docs/enums/data.Enums.StorageKey.html new file mode 100644 index 000000000..fc6e855aa --- /dev/null +++ b/frontend/docs/enums/data.Enums.StorageKey.html @@ -0,0 +1,3 @@ +StorageKey | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu
+

Keys in LocalStorage

+

Index

Enumeration members

Enumeration members

BEARER_TOKEN = "bearerToken"

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/index.html b/frontend/docs/index.html index c858e1a64..2b754df8a 100644 --- a/frontend/docs/index.html +++ b/frontend/docs/index.html @@ -1,4 +1,4 @@ -frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

frontend

+OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

OSOC 3 - Frontend Documentation

Frontend

@@ -104,4 +104,4 @@

yarn build

Builds the app for production to the build folder. It correctly bundles React in production mode and optimizes the build for the best performance.

The build is minified and the filenames include the hashes.

-

Settings

Theme

Generated using TypeDoc

\ No newline at end of file +

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/interfaces/Contexts.AuthContextState.html b/frontend/docs/interfaces/Contexts.AuthContextState.html deleted file mode 100644 index bcf39e00e..000000000 --- a/frontend/docs/interfaces/Contexts.AuthContextState.html +++ /dev/null @@ -1 +0,0 @@ -AuthContextState | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface AuthContextState

Hierarchy

  • AuthContextState

Index

Properties

editions: number[]
isLoggedIn: null | boolean
role: null | Role
token: null | string

Methods

  • setEditions(value: number[]): void
  • setIsLoggedIn(value: null | boolean): void
  • setRole(value: null | Role): void
  • setToken(value: null | string): void

Legend

  • Property
  • Method

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/interfaces/Data.Interfaces.User.html b/frontend/docs/interfaces/Data.Interfaces.User.html deleted file mode 100644 index cd05deb26..000000000 --- a/frontend/docs/interfaces/Data.Interfaces.User.html +++ /dev/null @@ -1 +0,0 @@ -User | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • User

Index

Properties

admin: boolean
editions: string[]
name: string
userId: number

Legend

  • Property

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/interfaces/contexts.AuthContextState.html b/frontend/docs/interfaces/contexts.AuthContextState.html new file mode 100644 index 000000000..26d0b2499 --- /dev/null +++ b/frontend/docs/interfaces/contexts.AuthContextState.html @@ -0,0 +1 @@ +AuthContextState | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • AuthContextState

Index

Properties

editions: number[]
isLoggedIn: null | boolean
role: null | Role
token: null | string

Methods

  • setEditions(value: number[]): void
  • setIsLoggedIn(value: null | boolean): void
  • setRole(value: null | Role): void
  • setToken(value: null | string): void

Legend

  • Interface
  • Property
  • Method
  • Namespace
  • Function

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/interfaces/data.Interfaces.User.html b/frontend/docs/interfaces/data.Interfaces.User.html new file mode 100644 index 000000000..df34564ba --- /dev/null +++ b/frontend/docs/interfaces/data.Interfaces.User.html @@ -0,0 +1 @@ +User | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • User

Index

Properties

admin: boolean
editions: string[]
name: string
userId: number

Legend

  • Namespace
  • Function
  • Interface
  • Property

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules.html b/frontend/docs/modules.html index 017a347fc..e60652d02 100644 --- a/frontend/docs/modules.html +++ b/frontend/docs/modules.html @@ -1 +1 @@ -frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

frontend

Settings

Theme

Generated using TypeDoc

\ No newline at end of file +OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

OSOC 3 - Frontend Documentation

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/App.html b/frontend/docs/modules/App.html new file mode 100644 index 000000000..93b578b20 --- /dev/null +++ b/frontend/docs/modules/App.html @@ -0,0 +1 @@ +App | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Functions

Functions

  • default(): Element

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Components.Login.html b/frontend/docs/modules/Components.Login.html deleted file mode 100644 index 36b4158a2..000000000 --- a/frontend/docs/modules/Components.Login.html +++ /dev/null @@ -1 +0,0 @@ -Login | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Functions

  • Email(__namedParameters: { email: string; setEmail: any }): Element
  • Password(__namedParameters: { password: string; callLogIn: any; setPassword: any }): Element
  • SocialButtons(): Element
  • WelcomeText(): Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Components.Register.html b/frontend/docs/modules/Components.Register.html deleted file mode 100644 index 74ace47ba..000000000 --- a/frontend/docs/modules/Components.Register.html +++ /dev/null @@ -1 +0,0 @@ -Register | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Register

Index

Functions

  • BadInviteLink(): Element
  • ConfirmPassword(__namedParameters: { confirmPassword: string; callRegister: any; setConfirmPassword: any }): Element
  • Email(__namedParameters: { email: string; setEmail: any }): Element
  • InfoText(): Element
  • Name(__namedParameters: { name: string; setName: any }): Element
  • Password(__namedParameters: { password: string; setPassword: any }): Element
  • SocialButtons(): Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Components.html b/frontend/docs/modules/Components.html deleted file mode 100644 index 097501732..000000000 --- a/frontend/docs/modules/Components.html +++ /dev/null @@ -1,7 +0,0 @@ -Components | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Components

Index

Functions

  • AdminRoute(): Element
  • Footer(): Element
  • NavBar(): Element
  • OSOCLetters(): Element
  • PrivateRoute(): Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Contexts.html b/frontend/docs/modules/Contexts.html deleted file mode 100644 index 9682d8a18..000000000 --- a/frontend/docs/modules/Contexts.html +++ /dev/null @@ -1,6 +0,0 @@ -Contexts | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Contexts

Index

Interfaces

Functions

Functions

  • AuthProvider(__namedParameters: { children: ReactNode }): Element
  • -

    Provider for auth that creates getters, setters, maintains state, and -provides default values

    -

    Not strictly necessary but keeps the main App clean by handling this -code here instead

    -

    Parameters

    • __namedParameters: { children: ReactNode }
      • children: ReactNode

    Returns Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Data.Enums.html b/frontend/docs/modules/Data.Enums.html deleted file mode 100644 index 7796028bb..000000000 --- a/frontend/docs/modules/Data.Enums.html +++ /dev/null @@ -1 +0,0 @@ -Enums | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Enums

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Data.Interfaces.html b/frontend/docs/modules/Data.Interfaces.html deleted file mode 100644 index eedd94daa..000000000 --- a/frontend/docs/modules/Data.Interfaces.html +++ /dev/null @@ -1 +0,0 @@ -Interfaces | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Interfaces

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Data.html b/frontend/docs/modules/Data.html deleted file mode 100644 index 870bc21f4..000000000 --- a/frontend/docs/modules/Data.html +++ /dev/null @@ -1 +0,0 @@ -Data | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Data

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Router.html b/frontend/docs/modules/Router.html new file mode 100644 index 000000000..ce7d88f7a --- /dev/null +++ b/frontend/docs/modules/Router.html @@ -0,0 +1 @@ +Router | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Functions

Functions

  • default(): Element

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Utils.Api.html b/frontend/docs/modules/Utils.Api.html deleted file mode 100644 index 630f3508d..000000000 --- a/frontend/docs/modules/Utils.Api.html +++ /dev/null @@ -1,6 +0,0 @@ -Api | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Api

Index

Functions

  • setBearerToken(value: null | string): void
  • -

    Set the default bearer token in the request headers

    -

    Parameters

    • value: null | string

    Returns void

  • validateRegistrationUrl(edition: string, uuid: string): Promise<boolean>
  • -

    Check if a registration url exists by sending a GET to it, -if it returns a 200 then we know the url is valid.

    -

    Parameters

    • edition: string
    • uuid: string

    Returns Promise<boolean>

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Utils.LocalStorage.html b/frontend/docs/modules/Utils.LocalStorage.html deleted file mode 100644 index 0c1f6a38c..000000000 --- a/frontend/docs/modules/Utils.LocalStorage.html +++ /dev/null @@ -1,6 +0,0 @@ -LocalStorage | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace LocalStorage

Index

Functions

  • getToken(): string | null
  • -

    Pull the user's token out of LocalStorage -Returns null if there is no token in LocalStorage yet

    -

    Returns string | null

  • setToken(value: string): void

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Utils.html b/frontend/docs/modules/Utils.html deleted file mode 100644 index 6f5cbf369..000000000 --- a/frontend/docs/modules/Utils.html +++ /dev/null @@ -1 +0,0 @@ -Utils | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Utils

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Views.Errors.html b/frontend/docs/modules/Views.Errors.html deleted file mode 100644 index d6be5ee1a..000000000 --- a/frontend/docs/modules/Views.Errors.html +++ /dev/null @@ -1 +0,0 @@ -Errors | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Errors

Index

Functions

Functions

  • NotFoundPage(): Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/Views.html b/frontend/docs/modules/Views.html deleted file mode 100644 index c6c985473..000000000 --- a/frontend/docs/modules/Views.html +++ /dev/null @@ -1 +0,0 @@ -Views | frontend
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace Views

Index

Functions

  • LoginPage(): Element
  • PendingPage(): Element
  • ProjectsPage(): Element
  • RegisterPage(): Element
  • StudentsPage(): Element
  • UsersPage(): Element
  • VerifyingTokenPage(): Element

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/components.LoginComponents.html b/frontend/docs/modules/components.LoginComponents.html new file mode 100644 index 000000000..7f2684cce --- /dev/null +++ b/frontend/docs/modules/components.LoginComponents.html @@ -0,0 +1,7 @@ +LoginComponents | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Functions

  • Email(__namedParameters: { email: string; setEmail: any }): Element
  • Password(__namedParameters: { password: string; callLogIn: any; setPassword: any }): Element
  • SocialButtons(): Element
  • WelcomeText(): Element

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/components.RegisterComponents.html b/frontend/docs/modules/components.RegisterComponents.html new file mode 100644 index 000000000..6e75288dc --- /dev/null +++ b/frontend/docs/modules/components.RegisterComponents.html @@ -0,0 +1 @@ +RegisterComponents | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Functions

  • BadInviteLink(): Element
  • ConfirmPassword(__namedParameters: { confirmPassword: string; callRegister: any; setConfirmPassword: any }): Element
  • Email(__namedParameters: { email: string; setEmail: any }): Element
  • InfoText(): Element
  • Name(__namedParameters: { name: string; setName: any }): Element
  • Password(__namedParameters: { password: string; setPassword: any }): Element
  • SocialButtons(): Element

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/components.html b/frontend/docs/modules/components.html new file mode 100644 index 000000000..37544e7c5 --- /dev/null +++ b/frontend/docs/modules/components.html @@ -0,0 +1,11 @@ +components | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Functions

  • AdminRoute(): Element
  • Footer(): Element
  • +

    Footer placed at the bottom of the site, containing various links related +to the application or our code.

    +

    The footer is only displayed when signed in.

    +

    Returns Element

  • NavBar(): Element
  • OSOCLetters(): Element
  • PrivateRoute(): Element

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/contexts.html b/frontend/docs/modules/contexts.html new file mode 100644 index 000000000..f352d48a4 --- /dev/null +++ b/frontend/docs/modules/contexts.html @@ -0,0 +1,6 @@ +contexts | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Interfaces

Functions

Functions

  • AuthProvider(__namedParameters: { children: ReactNode }): Element
  • +

    Provider for auth that creates getters, setters, maintains state, and +provides default values

    +

    Not strictly necessary but keeps the main App clean by handling this +code here instead

    +

    Parameters

    • __namedParameters: { children: ReactNode }
      • children: ReactNode

    Returns Element

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/data.Enums.html b/frontend/docs/modules/data.Enums.html new file mode 100644 index 000000000..cec181a81 --- /dev/null +++ b/frontend/docs/modules/data.Enums.html @@ -0,0 +1 @@ +Enums | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/data.Interfaces.html b/frontend/docs/modules/data.Interfaces.html new file mode 100644 index 000000000..9efdf085f --- /dev/null +++ b/frontend/docs/modules/data.Interfaces.html @@ -0,0 +1 @@ +Interfaces | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/data.html b/frontend/docs/modules/data.html new file mode 100644 index 000000000..40be88900 --- /dev/null +++ b/frontend/docs/modules/data.html @@ -0,0 +1 @@ +data | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/utils.Api.html b/frontend/docs/modules/utils.Api.html new file mode 100644 index 000000000..afac3a6d1 --- /dev/null +++ b/frontend/docs/modules/utils.Api.html @@ -0,0 +1,6 @@ +Api | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Functions

  • setBearerToken(value: null | string): void
  • +

    Set the default bearer token in the request headers

    +

    Parameters

    • value: null | string

    Returns void

  • validateRegistrationUrl(edition: string, uuid: string): Promise<boolean>
  • +

    Check if a registration url exists by sending a GET to it, +if it returns a 200 then we know the url is valid.

    +

    Parameters

    • edition: string
    • uuid: string

    Returns Promise<boolean>

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/utils.LocalStorage.html b/frontend/docs/modules/utils.LocalStorage.html new file mode 100644 index 000000000..c3fc7fc67 --- /dev/null +++ b/frontend/docs/modules/utils.LocalStorage.html @@ -0,0 +1,6 @@ +LocalStorage | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Functions

  • getToken(): string | null
  • +

    Pull the user's token out of LocalStorage +Returns null if there is no token in LocalStorage yet

    +

    Returns string | null

  • setToken(value: string): void

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/utils.html b/frontend/docs/modules/utils.html new file mode 100644 index 000000000..5274236f6 --- /dev/null +++ b/frontend/docs/modules/utils.html @@ -0,0 +1 @@ +utils | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/views.Errors.html b/frontend/docs/modules/views.Errors.html new file mode 100644 index 000000000..c5f548cae --- /dev/null +++ b/frontend/docs/modules/views.Errors.html @@ -0,0 +1 @@ +Errors | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Functions

  • ForbiddenPage(): Element
  • NotFoundPage(): Element

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/docs/modules/views.html b/frontend/docs/modules/views.html new file mode 100644 index 000000000..541f8d64f --- /dev/null +++ b/frontend/docs/modules/views.html @@ -0,0 +1 @@ +views | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Functions

  • LoginPage(): Element
  • PendingPage(): Element
  • ProjectsPage(): Element
  • RegisterPage(): Element
  • StudentsPage(): Element
  • UsersPage(): Element
  • VerifyingTokenPage(): Element

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file diff --git a/frontend/src/components/AdminRoute/AdminRoute.tsx b/frontend/src/components/AdminRoute/AdminRoute.tsx index 0e027003d..8133229ac 100644 --- a/frontend/src/components/AdminRoute/AdminRoute.tsx +++ b/frontend/src/components/AdminRoute/AdminRoute.tsx @@ -4,8 +4,8 @@ import { Role } from "../../data/enums"; /** * React component for admin-only routes - * Goes to login page if not authenticated, and to 403 - * if not admin + * Goes to the [[LoginPage]] if not authenticated, and to the [[ForbiddenPage]] + * (status 403) if not admin */ export default function AdminRoute() { const { isLoggedIn, role } = useAuth(); diff --git a/frontend/src/components/Footer/Footer.tsx b/frontend/src/components/Footer/Footer.tsx index 811a5a337..5a7ba9bdd 100644 --- a/frontend/src/components/Footer/Footer.tsx +++ b/frontend/src/components/Footer/Footer.tsx @@ -1,6 +1,12 @@ import { FooterBox, FooterTitle } from "./styles"; import FooterLinks from "./FooterLinks"; +/** + * Footer placed at the bottom of the site, containing various links related + * to the application or our code. + * + * The footer is only displayed when signed in. + */ export default function Footer() { return ( diff --git a/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx b/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx index 2a844b5b1..835dca6c4 100644 --- a/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx +++ b/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx @@ -1,5 +1,10 @@ import { Input } from "../styles"; +/** + * Input field for email addresses + * @param email getter for the state of the email address + * @param setEmail setter for the state of the email address + */ export default function Email({ email, setEmail, diff --git a/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx b/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx index ff044402f..94ee8b8f8 100644 --- a/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx +++ b/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx @@ -1,5 +1,11 @@ import { Input } from "../styles"; +/** + * Input field for passwords, authenticates when pressing the Enter key + * @param password getter for the state of the password + * @param setPassword setter for the state of the password + * @param callLogIn callback that tries to authenticate the user + */ export default function Password({ password, setPassword, diff --git a/frontend/src/components/LoginComponents/SocialButtons/SocialButtons.tsx b/frontend/src/components/LoginComponents/SocialButtons/SocialButtons.tsx index 3cca2d539..8ce39a557 100644 --- a/frontend/src/components/LoginComponents/SocialButtons/SocialButtons.tsx +++ b/frontend/src/components/LoginComponents/SocialButtons/SocialButtons.tsx @@ -1,6 +1,9 @@ import { GoogleLoginButton, GithubLoginButton } from "react-social-login-buttons"; import { SocialsContainer, Socials, GoogleLoginContainer } from "./styles"; +/** + * Container for the _Sign in with Google_ and _Sign in with GitHub_ buttons. + */ export default function SocialButtons() { return ( diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 546f604dc..ffe2ef414 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -1,7 +1,7 @@ export { default as AdminRoute } from "./AdminRoute"; export { default as Footer } from "./Footer"; -export * as Login from "./LoginComponents"; +export * as LoginComponents from "./LoginComponents"; export { default as NavBar } from "./navbar"; export { default as OSOCLetters } from "./OSOCLetters"; export { default as PrivateRoute } from "./PrivateRoute"; -export * as Register from "./RegisterComponents"; +export * as RegisterComponents from "./RegisterComponents"; diff --git a/frontend/src/index.ts b/frontend/src/index.ts deleted file mode 100644 index 36e681944..000000000 --- a/frontend/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * as Components from "./components"; -export * as Contexts from "./contexts"; -export * as Data from "./data"; -export * as Utils from "./utils"; -export * as Views from "./views"; diff --git a/frontend/src/views/errors/index.ts b/frontend/src/views/errors/index.ts index 67c80d24a..c46258f0f 100644 --- a/frontend/src/views/errors/index.ts +++ b/frontend/src/views/errors/index.ts @@ -1 +1,2 @@ +export { default as ForbiddenPage } from "./ForbiddenPage"; export { default as NotFoundPage } from "./NotFoundPage"; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 1798379bd..5a9dcb188 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -25,8 +25,15 @@ ], "typedocOptions": { "entryPoints": [ - "src/" + "src/App.tsx", + "src/Router.tsx", + "src/components", + "src/contexts", + "src/data", + "src/utils", + "src/views" ], + "name": "OSOC 3 - Frontend Documentation", "out": "docs" } } From ccda3da8442642b327172507f6f77aa6ff279631 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 3 Apr 2022 15:51:20 +0200 Subject: [PATCH 216/536] Add docs command to readme --- frontend/README.md | 73 ++++++++++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/frontend/README.md b/frontend/README.md index 1fe9b538c..c3f3b5f44 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -10,7 +10,8 @@ npm install --global yarn ``` -:heavy_exclamation_mark: Do **not** use `npm` anymore! Yarn and npm shouldn't be used at the same time. +:heavy_exclamation_mark: Do **not** use `npm` anymore! Yarn and npm shouldn't be used at the same +time. ```bash # Installing new package @@ -25,46 +26,62 @@ yarn install ## Setting up Prettier and ESLint -This directory contains configuration files for `Prettier` and `ESLint`, and depending on your IDE you may have to install or configure these in order for this to work. +This directory contains configuration files for `Prettier` and `ESLint`, and depending on your IDE +you may have to install or configure these in order for this to work. ### Prettier -Prettier is a code formatter that enforces your code to follow a specific style. Examples include automatically adding semicolons (;) at the end of every line, converting single-quoted strings ('a') to double-quoted strings (`"a"`), etc. +Prettier is a code formatter that enforces your code to follow a specific style. Examples include +automatically adding semicolons (;) at the end of every line, converting single-quoted strings ('a') +to double-quoted strings (`"a"`), etc. ### ESLint -ESLint is, as the name suggests, a linter that reviews your code for bad practices and ugly constructions. +ESLint is, as the name suggests, a linter that reviews your code for bad practices and ugly +constructions. ### JetBrains WebStorm -When using WebStorm, Prettier and ESLint are supported by default. ESLint is enabled automatically if a `.eslintrc` file is present, but you _do_ have to enable Prettier in the settings. +When using WebStorm, Prettier and ESLint are supported by default. ESLint is enabled automatically +if a `.eslintrc` file is present, but you _do_ have to enable Prettier in the settings. -1. Make sure the packages were installed by running `yarn install`, as `Prettier` and `ESLint` are among them. -2. In the search bar, type in "Prettier" (or navigate to `Languages & Frameworks > JavaScript > Prettier` manually). -3. If the `Prettier package`-field is still empty, click the dropdown. WebStorm should automatically list the Prettier from your local `node-modules` directory. +1. Make sure the packages were installed by running `yarn install`, as `Prettier` and `ESLint` are + among them. +2. In the search bar, type in "Prettier" (or navigate + to `Languages & Frameworks > JavaScript > Prettier` manually). +3. If the `Prettier package`-field is still empty, click the dropdown. WebStorm should automatically + list the Prettier from your local `node-modules` directory. 4. Select the `On 'Reformat Code' action` and `On save` checkboxes. ![Prettier WebStorm configuration](md-assets/readme/webstorm-prettier.png) ### Visual Studio Code -Visual Studio Code requires an extension for Prettier and ESLint to work, as they are not present in the editor. +Visual Studio Code requires an extension for Prettier and ESLint to work, as they are not present in +the editor. -1. Make sure the packages were installed by running `yarn install`, as `Prettier` and `ESLint` are among them +1. Make sure the packages were installed by running `yarn install`, as `Prettier` and `ESLint` are + among them -2. Install the [Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode). +2. Install + the [Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) + . -2. Install the [ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint). +2. Install + the [ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + . 3. Select `Prettier` as the default formatter in the `Editor: Default Formatter` dropdown option. - ![VSCode: Default Formatter setting](md-assets/readme/vscode-default-formatter.png) + ![VSCode: Default Formatter setting](md-assets/readme/vscode-default-formatter.png) 4. Enable the `Editor: Format On Save` option. - ![VSCode: Format On Save setting](md-assets/readme/vscode-format-on-save.png) + ![VSCode: Format On Save setting](md-assets/readme/vscode-format-on-save.png) -5. The path to the `Prettier` config file, and the module in `node_modules` should be detected **automatically**. In case it isn't (see [Try it out!](#try-it-out), you can always fill in the fields in `Prettier: Config Path` and `Prettier: Prettier Path`. +5. The path to the `Prettier` config file, and the module in `node_modules` should be detected ** + automatically**. In case it isn't (see [Try it out!](#try-it-out), you can always fill in the + fields in `Prettier: Config Path` and `Prettier: Prettier Path`. ### Try it out! @@ -74,18 +91,19 @@ To test if your new settings work, you can try the following: 2. In that file, add the following piece of code: - ```typescript - export const x = 5 // Don't add a semicolon here - - export function test() { - // "variable" is never used, and never reassigned - let variable = "something"; - } - ``` + ```typescript + export const x = 5 // Don't add a semicolon here + + export function test() { + // "variable" is never used, and never reassigned + let variable = "something"; + } + ``` 3. Save the file by pressing `ctrl + s` 4. Prettier: you should see a semicolon being added at the end of the line automatically -5. ESLint: you should get a warning on `variable` telling you that it was never used, and also that it should be marked as `const` because it's never reassigned. +5. ESLint: you should get a warning on `variable` telling you that it was never used, and also that + it should be marked as `const` because it's never reassigned. 6. Don't forget to remove the `test.ts` file again :) ## Available Scripts @@ -106,6 +124,11 @@ Launches the test runner. ### `yarn build` -Builds the app for production to the `build` folder. It correctly bundles React in production mode and optimizes the build for the best performance. +Builds the app for production to the `build` folder. It correctly bundles React in production mode +and optimizes the build for the best performance. The build is minified and the filenames include the hashes. + +### `yarn docs` + +Auto-generates documentation for our code. The output can be found in the `/docs` folder. From e362a6f42d6d96bbd688caf179a5fe7d69b9b81b Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 3 Apr 2022 16:26:29 +0200 Subject: [PATCH 217/536] Docstrings for components --- frontend/docs/enums/data.Enums.Role.html | 2 +- .../docs/enums/data.Enums.StorageKey.html | 2 +- frontend/docs/index.html | 60 +++++++++++++------ .../interfaces/contexts.AuthContextState.html | 2 +- .../docs/interfaces/data.Interfaces.User.html | 2 +- frontend/docs/modules.html | 2 +- frontend/docs/modules/App.html | 2 +- frontend/docs/modules/Router.html | 2 +- .../modules/components.LoginComponents.html | 10 ++-- .../components.RegisterComponents.html | 16 ++++- frontend/docs/modules/components.html | 25 ++++++-- frontend/docs/modules/contexts.html | 4 +- frontend/docs/modules/data.Enums.html | 2 +- frontend/docs/modules/data.Interfaces.html | 2 +- frontend/docs/modules/data.html | 2 +- frontend/docs/modules/utils.Api.html | 6 +- frontend/docs/modules/utils.LocalStorage.html | 6 +- frontend/docs/modules/utils.html | 2 +- frontend/docs/modules/views.Errors.html | 2 +- frontend/docs/modules/views.html | 2 +- .../src/components/AdminRoute/AdminRoute.tsx | 9 +++ .../WelcomeText/WelcomeText.tsx | 3 + .../components/OSOCLetters/OSOCLetters.tsx | 11 +++- .../components/PrivateRoute/PrivateRoute.tsx | 12 +++- .../BadInviteLink/BadInviteLink.tsx | 4 ++ .../RegisterComponents/InfoText/InfoText.tsx | 6 +- .../ConfirmPassword/ConfirmPassword.tsx | 6 ++ .../InputFields/Email/Email.tsx | 5 ++ .../InputFields/Name/Name.tsx | 5 ++ .../InputFields/Password/Password.tsx | 6 ++ frontend/src/components/navbar/NavBar.tsx | 8 ++- frontend/tsconfig.json | 1 + 32 files changed, 172 insertions(+), 57 deletions(-) diff --git a/frontend/docs/enums/data.Enums.Role.html b/frontend/docs/enums/data.Enums.Role.html index b4381e97f..1a9cc3e93 100644 --- a/frontend/docs/enums/data.Enums.Role.html +++ b/frontend/docs/enums/data.Enums.Role.html @@ -1,4 +1,4 @@ Role | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Enum for the different levels of authority a user can have

-

Index

Enumeration members

Enumeration members

ADMIN = 0
COACH = 1

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file +

Index

Enumeration members

Enumeration members

ADMIN = 0
COACH = 1

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

\ No newline at end of file diff --git a/frontend/docs/enums/data.Enums.StorageKey.html b/frontend/docs/enums/data.Enums.StorageKey.html index fc6e855aa..7c42e9c4c 100644 --- a/frontend/docs/enums/data.Enums.StorageKey.html +++ b/frontend/docs/enums/data.Enums.StorageKey.html @@ -1,3 +1,3 @@ StorageKey | OSOC 3 - Frontend Documentation
Options
All
  • Public
  • Public/Protected
  • All
Menu

Keys in LocalStorage

-

Index

Enumeration members

Enumeration members

BEARER_TOKEN = "bearerToken"

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

Generated using TypeDoc

\ No newline at end of file +

Index

Enumeration members

Enumeration members

BEARER_TOKEN = "bearerToken"

Legend

  • Namespace
  • Function
  • Interface

Settings

Theme

\ No newline at end of file diff --git a/frontend/docs/index.html b/frontend/docs/index.html index 2b754df8a..6ba31a5d3 100644 --- a/frontend/docs/index.html +++ b/frontend/docs/index.html @@ -10,33 +10,42 @@

Installing (and using) Yarn

- You install Yarn

npm install --global yarn
 
-

:heavy_exclamation_mark: Do not use npm anymore! Yarn and npm shouldn't be used at the same time.

+

:heavy_exclamation_mark: Do not use npm anymore! Yarn and npm shouldn't be used at the same +time.

# Installing new package
yarn add <package_name>

# Installing new package as a dev dependency
yarn add --dev <package_name>

# Installing all packages listed in package.json
yarn install

Setting up Prettier and ESLint

-

This directory contains configuration files for Prettier and ESLint, and depending on your IDE you may have to install or configure these in order for this to work.

+

This directory contains configuration files for Prettier and ESLint, and depending on your IDE +you may have to install or configure these in order for this to work.

Prettier

-

Prettier is a code formatter that enforces your code to follow a specific style. Examples include automatically adding semicolons (;) at the end of every line, converting single-quoted strings ('a') to double-quoted strings ("a"), etc.

+

Prettier is a code formatter that enforces your code to follow a specific style. Examples include +automatically adding semicolons (;) at the end of every line, converting single-quoted strings ('a') +to double-quoted strings ("a"), etc.

ESLint

-

ESLint is, as the name suggests, a linter that reviews your code for bad practices and ugly constructions.

+

ESLint is, as the name suggests, a linter that reviews your code for bad practices and ugly +constructions.

JetBrains WebStorm

-

When using WebStorm, Prettier and ESLint are supported by default. ESLint is enabled automatically if a .eslintrc file is present, but you do have to enable Prettier in the settings.

+

When using WebStorm, Prettier and ESLint are supported by default. ESLint is enabled automatically +if a .eslintrc file is present, but you do have to enable Prettier in the settings.

    -
  1. Make sure the packages were installed by running yarn install, as Prettier and ESLint are among them.
  2. -
  3. In the search bar, type in "Prettier" (or navigate to Languages & Frameworks > JavaScript > Prettier manually).
  4. -
  5. If the Prettier package-field is still empty, click the dropdown. WebStorm should automatically list the Prettier from your local node-modules directory.
  6. +
  7. Make sure the packages were installed by running yarn install, as Prettier and ESLint are +among them.
  8. +
  9. In the search bar, type in "Prettier" (or navigate +to Languages & Frameworks > JavaScript > Prettier manually).
  10. +
  11. If the Prettier package-field is still empty, click the dropdown. WebStorm should automatically +list the Prettier from your local node-modules directory.
  12. Select the On 'Reformat Code' action and On save checkboxes.

Prettier WebStorm configuration

@@ -44,21 +53,29 @@

JetBrains WebStorm

Visual Studio Code

-

Visual Studio Code requires an extension for Prettier and ESLint to work, as they are not present in the editor.

+

Visual Studio Code requires an extension for Prettier and ESLint to work, as they are not present in +the editor.

    -
  1. Make sure the packages were installed by running yarn install, as Prettier and ESLint are among them

    +
  2. Make sure the packages were installed by running yarn install, as Prettier and ESLint are +among them

  3. -
  4. Install the Prettier extension.

    +
  5. Install +the Prettier extension +.

  6. -
  7. Install the ESLint extension.

    +
  8. Install +the ESLint extension +.

  9. Select Prettier as the default formatter in the Editor: Default Formatter dropdown option.

    -

    VSCode: Default Formatter setting

    +

    VSCode: Default Formatter setting

  10. Enable the Editor: Format On Save option.

    -

    VSCode: Format On Save setting

    +

    VSCode: Format On Save setting

  11. -
  12. The path to the Prettier config file, and the module in node_modules should be detected automatically. In case it isn't (see Try it out!, you can always fill in the fields in Prettier: Config Path and Prettier: Prettier Path.

    +
  13. The path to the Prettier config file, and the module in node_modules should be detected ** +automatically**. In case it isn't (see Try it out!, you can always fill in the +fields in Prettier: Config Path and Prettier: Prettier Path.

@@ -77,7 +94,8 @@

Try it out!

  • Prettier: you should see a semicolon being added at the end of the line automatically

  • -
  • ESLint: you should get a warning on variable telling you that it was never used, and also that it should be marked as const because it's never reassigned.

    +
  • ESLint: you should get a warning on variable telling you that it was never used, and also that +it should be marked as const because it's never reassigned.

  • Don't forget to remove the test.ts file again :)

  • @@ -102,6 +120,12 @@

    yarn test

    yarn build

    -

    Builds the app for production to the build folder. It correctly bundles React in production mode and optimizes the build for the best performance.

    +

    Builds the app for production to the build folder. It correctly bundles React in production mode +and optimizes the build for the best performance.

    The build is minified and the filenames include the hashes.

    -

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file + + +

    yarn docs

    +
    +

    Auto-generates documentation for our code. The output can be found in the /docs folder.

    +

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/interfaces/contexts.AuthContextState.html b/frontend/docs/interfaces/contexts.AuthContextState.html index 26d0b2499..9d999ba65 100644 --- a/frontend/docs/interfaces/contexts.AuthContextState.html +++ b/frontend/docs/interfaces/contexts.AuthContextState.html @@ -1 +1 @@ -AuthContextState | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • AuthContextState

    Index

    Properties

    editions: number[]
    isLoggedIn: null | boolean
    role: null | Role
    token: null | string

    Methods

    • setEditions(value: number[]): void
    • setIsLoggedIn(value: null | boolean): void
    • setRole(value: null | Role): void
    • setToken(value: null | string): void

    Legend

    • Interface
    • Property
    • Method
    • Namespace
    • Function

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +AuthContextState | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • AuthContextState

    Index

    Properties

    editions: number[]
    isLoggedIn: null | boolean
    role: null | Role
    token: null | string

    Methods

    • setEditions(value: number[]): void
    • setIsLoggedIn(value: null | boolean): void
    • setRole(value: null | Role): void
    • setToken(value: null | string): void

    Legend

    • Interface
    • Property
    • Method
    • Namespace
    • Function

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/interfaces/data.Interfaces.User.html b/frontend/docs/interfaces/data.Interfaces.User.html index df34564ba..bf886d3d8 100644 --- a/frontend/docs/interfaces/data.Interfaces.User.html +++ b/frontend/docs/interfaces/data.Interfaces.User.html @@ -1 +1 @@ -User | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • User

    Index

    Properties

    admin: boolean
    editions: string[]
    name: string
    userId: number

    Legend

    • Namespace
    • Function
    • Interface
    • Property

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +User | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • User

    Index

    Properties

    admin: boolean
    editions: string[]
    name: string
    userId: number

    Legend

    • Namespace
    • Function
    • Interface
    • Property

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules.html b/frontend/docs/modules.html index e60652d02..a3cb69eea 100644 --- a/frontend/docs/modules.html +++ b/frontend/docs/modules.html @@ -1 +1 @@ -OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    OSOC 3 - Frontend Documentation

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    OSOC 3 - Frontend Documentation

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/App.html b/frontend/docs/modules/App.html index 93b578b20..cdd7a6fce 100644 --- a/frontend/docs/modules/App.html +++ b/frontend/docs/modules/App.html @@ -1 +1 @@ -App | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    Functions

    • default(): Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +App | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    Functions

    • default(): Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/Router.html b/frontend/docs/modules/Router.html index ce7d88f7a..468711e02 100644 --- a/frontend/docs/modules/Router.html +++ b/frontend/docs/modules/Router.html @@ -1 +1 @@ -Router | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    Functions

    • default(): Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +Router | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    Functions

    • default(): Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/components.LoginComponents.html b/frontend/docs/modules/components.LoginComponents.html index 7f2684cce..101b2bcc0 100644 --- a/frontend/docs/modules/components.LoginComponents.html +++ b/frontend/docs/modules/components.LoginComponents.html @@ -1,7 +1,9 @@ -LoginComponents | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    • Email(__namedParameters: { email: string; setEmail: any }): Element
    • WelcomeText(): Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/components.RegisterComponents.html b/frontend/docs/modules/components.RegisterComponents.html index 6e75288dc..e54881896 100644 --- a/frontend/docs/modules/components.RegisterComponents.html +++ b/frontend/docs/modules/components.RegisterComponents.html @@ -1 +1,15 @@ -RegisterComponents | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    • BadInviteLink(): Element
    • ConfirmPassword(__namedParameters: { confirmPassword: string; callRegister: any; setConfirmPassword: any }): Element
    • Email(__namedParameters: { email: string; setEmail: any }): Element
    • InfoText(): Element
    • Name(__namedParameters: { name: string; setName: any }): Element
    • Password(__namedParameters: { password: string; setPassword: any }): Element
    • SocialButtons(): Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +RegisterComponents | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    • BadInviteLink(): Element
    • ConfirmPassword(__namedParameters: { confirmPassword: string; callRegister: any; setConfirmPassword: any }): Element
    • Email(__namedParameters: { email: string; setEmail: any }): Element
    • InfoText(): Element
    • Name(__namedParameters: { name: string; setName: any }): Element
    • Password(__namedParameters: { password: string; setPassword: any }): Element
    • SocialButtons(): Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/components.html b/frontend/docs/modules/components.html index 37544e7c5..cf9f2bb89 100644 --- a/frontend/docs/modules/components.html +++ b/frontend/docs/modules/components.html @@ -1,11 +1,26 @@ -components | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    • AdminRoute(): Element
    • NavBar(): Element
    • +

      NavBar displayed at the top of the page. +Links are hidden if the user is not authorized to see them.

      +

      Returns Element

    • OSOCLetters(): Element
    • +

      Animated OSOC-letters, inspired by the ones found +on the OSOC website.

      +

      Note: This component is currently not in use because the positioning +of the letters causes issues. We have given priority to other parts of the application.

      +

      Returns Element

    • PrivateRoute(): Element
    • +

      React component that redirects to the LoginPage if not authenticated when +trying to visit a route.

      +

      Example usage:

      +
      <Route path={"/path"} element={<PrivateRoute />}>
      // These routes will only render if the user is authenticated
      <Route path={"/"} />
      <Route path={"/child"} />
      </Route> +
      +

      Returns Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/contexts.html b/frontend/docs/modules/contexts.html index f352d48a4..99e82bff9 100644 --- a/frontend/docs/modules/contexts.html +++ b/frontend/docs/modules/contexts.html @@ -1,6 +1,6 @@ -contexts | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Interfaces

    Functions

    Functions

    • AuthProvider(__namedParameters: { children: ReactNode }): Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/data.Enums.html b/frontend/docs/modules/data.Enums.html index cec181a81..4b652ff03 100644 --- a/frontend/docs/modules/data.Enums.html +++ b/frontend/docs/modules/data.Enums.html @@ -1 +1 @@ -Enums | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +Enums | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/data.Interfaces.html b/frontend/docs/modules/data.Interfaces.html index 9efdf085f..573148cd1 100644 --- a/frontend/docs/modules/data.Interfaces.html +++ b/frontend/docs/modules/data.Interfaces.html @@ -1 +1 @@ -Interfaces | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +Interfaces | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/data.html b/frontend/docs/modules/data.html index 40be88900..bac91033f 100644 --- a/frontend/docs/modules/data.html +++ b/frontend/docs/modules/data.html @@ -1 +1 @@ -data | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +data | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/utils.Api.html b/frontend/docs/modules/utils.Api.html index afac3a6d1..5e789d477 100644 --- a/frontend/docs/modules/utils.Api.html +++ b/frontend/docs/modules/utils.Api.html @@ -1,6 +1,6 @@ -Api | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    • setBearerToken(value: null | string): void

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/utils.LocalStorage.html b/frontend/docs/modules/utils.LocalStorage.html index c3fc7fc67..b11f6bfe0 100644 --- a/frontend/docs/modules/utils.LocalStorage.html +++ b/frontend/docs/modules/utils.LocalStorage.html @@ -1,6 +1,6 @@ -LocalStorage | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    • getToken(): string | null

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/utils.html b/frontend/docs/modules/utils.html index 5274236f6..29a8d186b 100644 --- a/frontend/docs/modules/utils.html +++ b/frontend/docs/modules/utils.html @@ -1 +1 @@ -utils | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +utils | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/views.Errors.html b/frontend/docs/modules/views.Errors.html index c5f548cae..43eb264e5 100644 --- a/frontend/docs/modules/views.Errors.html +++ b/frontend/docs/modules/views.Errors.html @@ -1 +1 @@ -Errors | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    • ForbiddenPage(): Element
    • NotFoundPage(): Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +Errors | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    • ForbiddenPage(): Element
    • NotFoundPage(): Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/views.html b/frontend/docs/modules/views.html index 541f8d64f..0ecfe0dfe 100644 --- a/frontend/docs/modules/views.html +++ b/frontend/docs/modules/views.html @@ -1 +1 @@ -views | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    • LoginPage(): Element
    • PendingPage(): Element
    • ProjectsPage(): Element
    • RegisterPage(): Element
    • StudentsPage(): Element
    • UsersPage(): Element
    • VerifyingTokenPage(): Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +views | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    • LoginPage(): Element
    • PendingPage(): Element
    • ProjectsPage(): Element
    • RegisterPage(): Element
    • StudentsPage(): Element
    • UsersPage(): Element
    • VerifyingTokenPage(): Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/src/components/AdminRoute/AdminRoute.tsx b/frontend/src/components/AdminRoute/AdminRoute.tsx index 8133229ac..5f11c352d 100644 --- a/frontend/src/components/AdminRoute/AdminRoute.tsx +++ b/frontend/src/components/AdminRoute/AdminRoute.tsx @@ -6,6 +6,15 @@ import { Role } from "../../data/enums"; * React component for admin-only routes * Goes to the [[LoginPage]] if not authenticated, and to the [[ForbiddenPage]] * (status 403) if not admin + * + * Example usage: + * ```ts + * }> + * // These routes will only render if the user is an admin + * + * + * + * ``` */ export default function AdminRoute() { const { isLoggedIn, role } = useAuth(); diff --git a/frontend/src/components/LoginComponents/WelcomeText/WelcomeText.tsx b/frontend/src/components/LoginComponents/WelcomeText/WelcomeText.tsx index 04be7e6c3..d19300738 100644 --- a/frontend/src/components/LoginComponents/WelcomeText/WelcomeText.tsx +++ b/frontend/src/components/LoginComponents/WelcomeText/WelcomeText.tsx @@ -1,5 +1,8 @@ import { WelcomeTextContainer } from "./styles"; +/** + * Text displayed on the [[LoginPage]] to welcome the users to the application. + */ export default function WelcomeText() { return ( diff --git a/frontend/src/components/OSOCLetters/OSOCLetters.tsx b/frontend/src/components/OSOCLetters/OSOCLetters.tsx index d4fedb382..72bc85d3f 100644 --- a/frontend/src/components/OSOCLetters/OSOCLetters.tsx +++ b/frontend/src/components/OSOCLetters/OSOCLetters.tsx @@ -4,7 +4,14 @@ import logoO2 from "../../images/letters/osoc_red_o.svg"; import logoC from "../../images/letters/osoc_c.svg"; import "./OSOCLetters.css"; -function OSOCLetters() { +/** + * Animated OSOC-letters, inspired by the ones found + * on the [OSOC website](https://osoc.be/). + * + * _Note: This component is currently not in use because the positioning + * of the letters causes issues. We have given priority to other parts of the application._ + */ +export default function OSOCLetters() { return (
    logoO1 @@ -14,5 +21,3 @@ function OSOCLetters() {
    ); } - -export default OSOCLetters; diff --git a/frontend/src/components/PrivateRoute/PrivateRoute.tsx b/frontend/src/components/PrivateRoute/PrivateRoute.tsx index 94a09f770..b61b34400 100644 --- a/frontend/src/components/PrivateRoute/PrivateRoute.tsx +++ b/frontend/src/components/PrivateRoute/PrivateRoute.tsx @@ -2,7 +2,17 @@ import { useAuth } from "../../contexts/auth-context"; import { Navigate, Outlet } from "react-router-dom"; /** - * React component that goes to the login page if not authenticated + * React component that redirects to the [[LoginPage]] if not authenticated when + * trying to visit a route. + * + * Example usage: + * ```ts + * }> + * // These routes will only render if the user is authenticated + * + * + * + * ``` */ export default function PrivateRoute() { const { isLoggedIn } = useAuth(); diff --git a/frontend/src/components/RegisterComponents/BadInviteLink/BadInviteLink.tsx b/frontend/src/components/RegisterComponents/BadInviteLink/BadInviteLink.tsx index def284545..8ad2c2887 100644 --- a/frontend/src/components/RegisterComponents/BadInviteLink/BadInviteLink.tsx +++ b/frontend/src/components/RegisterComponents/BadInviteLink/BadInviteLink.tsx @@ -1,5 +1,9 @@ import { BadInvite } from "./styles"; +/** + * Message displayed when the user tries to access a registration link + * that doesn't exist (anymore), for example `{{BASE_URL}}/register/this-is-not-a-valid-uuid`. + */ export default function BadInviteLink() { return Not a valid register url.; } diff --git a/frontend/src/components/RegisterComponents/InfoText/InfoText.tsx b/frontend/src/components/RegisterComponents/InfoText/InfoText.tsx index ced927301..e193eb9e4 100644 --- a/frontend/src/components/RegisterComponents/InfoText/InfoText.tsx +++ b/frontend/src/components/RegisterComponents/InfoText/InfoText.tsx @@ -1,12 +1,16 @@ import { TitleText, Info } from "./styles"; +/** + * Message displayed on the [[RegisterPage]] to inform the user of what + * to do, and provides some additional info on the registration links. + */ export default function InfoText() { return (
    Create an account Sign up with your social media account or email address. Your unique link is not - useable again. + re-usable.
    ); diff --git a/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx b/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx index 40383be78..a5e7489b5 100644 --- a/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx +++ b/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx @@ -1,5 +1,11 @@ import { Input } from "../styles"; +/** + * Input field for passwords (confirmation), submits when pressing the Enter key + * @param confirmPassword getter for the state of the password + * @param setConfirmPassword setter for the state of the password + * @param callRegister callback that tries to register the user + */ export default function ConfirmPassword({ confirmPassword, setConfirmPassword, diff --git a/frontend/src/components/RegisterComponents/InputFields/Email/Email.tsx b/frontend/src/components/RegisterComponents/InputFields/Email/Email.tsx index 2a844b5b1..835dca6c4 100644 --- a/frontend/src/components/RegisterComponents/InputFields/Email/Email.tsx +++ b/frontend/src/components/RegisterComponents/InputFields/Email/Email.tsx @@ -1,5 +1,10 @@ import { Input } from "../styles"; +/** + * Input field for email addresses + * @param email getter for the state of the email address + * @param setEmail setter for the state of the email address + */ export default function Email({ email, setEmail, diff --git a/frontend/src/components/RegisterComponents/InputFields/Name/Name.tsx b/frontend/src/components/RegisterComponents/InputFields/Name/Name.tsx index 35ceedee3..72b9ffedf 100644 --- a/frontend/src/components/RegisterComponents/InputFields/Name/Name.tsx +++ b/frontend/src/components/RegisterComponents/InputFields/Name/Name.tsx @@ -1,5 +1,10 @@ import { Input } from "../styles"; +/** + * Input field for the user's name + * @param name getter for the state of the name + * @param setName setter for the state of the name + */ export default function Name({ name, setName, diff --git a/frontend/src/components/RegisterComponents/InputFields/Password/Password.tsx b/frontend/src/components/RegisterComponents/InputFields/Password/Password.tsx index 799004c22..b3f2802c2 100644 --- a/frontend/src/components/RegisterComponents/InputFields/Password/Password.tsx +++ b/frontend/src/components/RegisterComponents/InputFields/Password/Password.tsx @@ -1,5 +1,11 @@ import { Input } from "../styles"; +/** + * Input field for passwords, authenticates when pressing the Enter key + * @param password getter for the state of the password + * @param setPassword setter for the state of the password + * @param callLogIn callback that tries to authenticate the user + */ export default function Password({ password, setPassword, diff --git a/frontend/src/components/navbar/NavBar.tsx b/frontend/src/components/navbar/NavBar.tsx index 76a3e54cb..eccd944c1 100644 --- a/frontend/src/components/navbar/NavBar.tsx +++ b/frontend/src/components/navbar/NavBar.tsx @@ -3,7 +3,11 @@ import { Bars, Nav, NavLink, NavMenu } from "./NavBarElements"; import "./navbar.css"; import { useAuth } from "../../contexts/auth-context"; -function NavBar() { +/** + * NavBar displayed at the top of the page. + * Links are hidden if the user is not authorized to see them. + */ +export default function NavBar() { const { token, setToken } = useAuth(); const hidden = token ? "nav-links" : "nav-hidden"; @@ -39,5 +43,3 @@ function NavBar() { ); } - -export default NavBar; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 5a9dcb188..543c9d04d 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -24,6 +24,7 @@ "src" ], "typedocOptions": { + "hideGenerator": true, "entryPoints": [ "src/App.tsx", "src/Router.tsx", From 45a8966a7536df60d0be9a54180d9815c42924e3 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 3 Apr 2022 16:40:13 +0200 Subject: [PATCH 218/536] more docstrings --- frontend/docs/enums/data.Enums.Role.html | 2 +- .../docs/enums/data.Enums.StorageKey.html | 6 +++-- .../interfaces/contexts.AuthContextState.html | 4 +++- .../docs/interfaces/data.Interfaces.User.html | 6 ++++- frontend/docs/modules/App.html | 2 +- frontend/docs/modules/Router.html | 2 +- .../modules/components.LoginComponents.html | 12 +++++----- .../components.RegisterComponents.html | 22 +++++++++---------- frontend/docs/modules/components.html | 16 +++++++------- frontend/docs/modules/contexts.html | 8 +++---- frontend/docs/modules/utils.Api.html | 11 +++++----- frontend/docs/modules/utils.LocalStorage.html | 4 ++-- frontend/docs/modules/views.Errors.html | 2 +- frontend/docs/modules/views.html | 2 +- .../src/components/AdminRoute/AdminRoute.tsx | 6 ++--- .../InputFields/Email/Email.tsx | 2 +- .../InputFields/Password/Password.tsx | 2 +- .../ConfirmPassword/ConfirmPassword.tsx | 2 +- .../InputFields/Email/Email.tsx | 2 +- .../InputFields/Name/Name.tsx | 2 +- .../InputFields/Password/Password.tsx | 2 +- frontend/src/contexts/auth-context.tsx | 15 ++++++++----- frontend/src/data/enums/local-storage.ts | 5 ++++- frontend/src/data/interfaces/users.ts | 5 +++++ frontend/src/utils/api/api.ts | 3 ++- frontend/src/utils/api/auth.ts | 8 +++---- frontend/src/utils/api/login.ts | 7 ++++++ 27 files changed, 95 insertions(+), 65 deletions(-) diff --git a/frontend/docs/enums/data.Enums.Role.html b/frontend/docs/enums/data.Enums.Role.html index 1a9cc3e93..63cd2b0b4 100644 --- a/frontend/docs/enums/data.Enums.Role.html +++ b/frontend/docs/enums/data.Enums.Role.html @@ -1,4 +1,4 @@ Role | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Enum for the different levels of authority a user can have

    -

    Index

    Enumeration members

    Enumeration members

    ADMIN = 0
    COACH = 1

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file +

    Index

    Enumeration members

    Enumeration members

    ADMIN = 0
    COACH = 1

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/enums/data.Enums.StorageKey.html b/frontend/docs/enums/data.Enums.StorageKey.html index 7c42e9c4c..c82d6495e 100644 --- a/frontend/docs/enums/data.Enums.StorageKey.html +++ b/frontend/docs/enums/data.Enums.StorageKey.html @@ -1,3 +1,5 @@ StorageKey | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu
    -

    Keys in LocalStorage

    -

    Index

    Enumeration members

    Enumeration members

    BEARER_TOKEN = "bearerToken"

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file +

    Enum for the keys in LocalStorage.

    +

    Index

    Enumeration members

    Enumeration members

    BEARER_TOKEN = "bearerToken"
    +

    Bearer token used to authorize the user's requests in the backend.

    +

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/interfaces/contexts.AuthContextState.html b/frontend/docs/interfaces/contexts.AuthContextState.html index 9d999ba65..8dbf7bb88 100644 --- a/frontend/docs/interfaces/contexts.AuthContextState.html +++ b/frontend/docs/interfaces/contexts.AuthContextState.html @@ -1 +1,3 @@ -AuthContextState | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • AuthContextState

    Index

    Properties

    editions: number[]
    isLoggedIn: null | boolean
    role: null | Role
    token: null | string

    Methods

    • setEditions(value: number[]): void
    • setIsLoggedIn(value: null | boolean): void
    • setRole(value: null | Role): void
    • setToken(value: null | string): void

    Legend

    • Interface
    • Property
    • Method
    • Namespace
    • Function

    Settings

    Theme

    \ No newline at end of file +AuthContextState | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu
    +

    Interface that holds the data stored in the AuthContext.

    +

    Hierarchy

    • AuthContextState

    Index

    Properties

    editions: number[]
    isLoggedIn: null | boolean
    role: null | Role
    token: null | string

    Methods

    • setEditions(value: number[]): void
    • setIsLoggedIn(value: null | boolean): void
    • setRole(value: null | Role): void
    • setToken(value: null | string): void

    Legend

    • Interface
    • Property
    • Method
    • Namespace
    • Function

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/interfaces/data.Interfaces.User.html b/frontend/docs/interfaces/data.Interfaces.User.html index bf886d3d8..bf66f6bbc 100644 --- a/frontend/docs/interfaces/data.Interfaces.User.html +++ b/frontend/docs/interfaces/data.Interfaces.User.html @@ -1 +1,5 @@ -User | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • User

    Index

    Properties

    admin: boolean
    editions: string[]
    name: string
    userId: number

    Legend

    • Namespace
    • Function
    • Interface
    • Property

    Settings

    Theme

    \ No newline at end of file +User | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu
    +

    Data about a user using the application. +Contains a list of edition names so that we can quickly check if +they have access to a route or not.

    +

    Hierarchy

    • User

    Index

    Properties

    admin: boolean
    editions: string[]
    name: string
    userId: number

    Legend

    • Namespace
    • Function
    • Interface
    • Property

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/App.html b/frontend/docs/modules/App.html index cdd7a6fce..70c121b16 100644 --- a/frontend/docs/modules/App.html +++ b/frontend/docs/modules/App.html @@ -1 +1 @@ -App | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    Functions

    • default(): Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file +App | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    Functions

    • default(): Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/Router.html b/frontend/docs/modules/Router.html index 468711e02..dfd049368 100644 --- a/frontend/docs/modules/Router.html +++ b/frontend/docs/modules/Router.html @@ -1 +1 @@ -Router | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    Functions

    • default(): Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file +Router | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    Functions

    • default(): Element

    Legend

    • Namespace
    • Function
    • Interface

    Settings

    Theme

    \ No newline at end of file diff --git a/frontend/docs/modules/components.LoginComponents.html b/frontend/docs/modules/components.LoginComponents.html index 101b2bcc0..96385af66 100644 --- a/frontend/docs/modules/components.LoginComponents.html +++ b/frontend/docs/modules/components.LoginComponents.html @@ -1,9 +1,9 @@ -LoginComponents | OSOC 3 - Frontend Documentation
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Functions

    • Email(__namedParameters: { email: string; setEmail: any }): Element
    • Password(__namedParameters: { password: string; callLogIn: any; setPassword: any }): Element
    • SocialButtons(): Element
    • +LoginComponents | OSOC 3 - Frontend Documentation
      Options
      All
      • Public
      • Public/Protected
      • All
      Menu

      Index

      Functions

      • Email(__namedParameters: { email: string; setEmail: any }): Element
      • Password(__namedParameters: { password: string; callLogIn: any; setPassword: any }): Element
      • SocialButtons(): Element
      • WelcomeText(): Element
      • WelcomeText(): Element

      Legend

      • Namespace
      • Function
      • Interface

      Settings

      Theme

      \ No newline at end of file diff --git a/frontend/docs/modules/components.RegisterComponents.html b/frontend/docs/modules/components.RegisterComponents.html index e54881896..dddd0e1af 100644 --- a/frontend/docs/modules/components.RegisterComponents.html +++ b/frontend/docs/modules/components.RegisterComponents.html @@ -1,15 +1,15 @@ -RegisterComponents | OSOC 3 - Frontend Documentation
      Options
      All
      • Public
      • Public/Protected
      • All
      Menu

      Index

      Functions

      • BadInviteLink(): Element
      • Name(__namedParameters: { name: string; setName: any }): Element
      • Password(__namedParameters: { password: string; setPassword: any }): Element
      • SocialButtons(): Element

      Legend

      • Namespace
      • Function
      • Interface

      Settings

      Theme

      \ No newline at end of file diff --git a/frontend/docs/modules/components.html b/frontend/docs/modules/components.html index cf9f2bb89..2bfe959da 100644 --- a/frontend/docs/modules/components.html +++ b/frontend/docs/modules/components.html @@ -1,23 +1,23 @@ -components | OSOC 3 - Frontend Documentation
      Options
      All
      • Public
      • Public/Protected
      • All
      Menu

      Index

      Functions

      • AdminRoute(): Element
      • -

        React component for admin-only routes -Goes to the LoginPage if not authenticated, and to the ForbiddenPage -(status 403) if not admin

        +components | OSOC 3 - Frontend Documentation
        Options
        All
        • Public
        • Public/Protected
        • All
        Menu

        Index

        Functions

        • AdminRoute(): Element
        • +

          React component for admin-only routes. +Redirects to the LoginPage (status 401) if not authenticated, +and to the ForbiddenPage (status 403) if not admin.

          Example usage:

          <Route path={"/path"} element={<AdminRoute />}>
          // These routes will only render if the user is an admin
          <Route path={"/"} />
          <Route path={"/child"} />
          </Route>
          -

          Returns Element

        • Footer(): Element
        • Footer(): Element
        • Footer placed at the bottom of the site, containing various links related to the application or our code.

          The footer is only displayed when signed in.

          -

          Returns Element

        • NavBar(): Element
        • NavBar(): Element
        • NavBar displayed at the top of the page. Links are hidden if the user is not authorized to see them.

          -

          Returns Element

        • OSOCLetters(): Element
        • OSOCLetters(): Element
        • Animated OSOC-letters, inspired by the ones found on the OSOC website.

          Note: This component is currently not in use because the positioning of the letters causes issues. We have given priority to other parts of the application.

          -

          Returns Element

        • PrivateRoute(): Element
        • PrivateRoute(): Element
        • React component that redirects to the LoginPage if not authenticated when trying to visit a route.

          Example usage:

          diff --git a/frontend/docs/modules/contexts.html b/frontend/docs/modules/contexts.html index 99e82bff9..e77922072 100644 --- a/frontend/docs/modules/contexts.html +++ b/frontend/docs/modules/contexts.html @@ -1,6 +1,6 @@ -contexts | OSOC 3 - Frontend Documentation
          Options
          All
          • Public
          • Public/Protected
          • All
          Menu

          Index

          Interfaces

          Functions

          Functions

          • AuthProvider(__namedParameters: { children: ReactNode }): Element
          • +contexts | OSOC 3 - Frontend Documentation
            Options
            All
            • Public
            • Public/Protected
            • All
            Menu

            Index

            Interfaces

            Functions

            Functions

            • AuthProvider(__namedParameters: { children: ReactNode }): Element
            • Provider for auth that creates getters, setters, maintains state, and -provides default values

              -

              Not strictly necessary but keeps the main App clean by handling this -code here instead

              +provides default values.

              +

              This keeps the main App component code clean by handling this +boilerplate here instead.

              Parameters

              • __namedParameters: { children: ReactNode }
                • children: ReactNode

              Returns Element

            Legend

            • Namespace
            • Function
            • Interface

            Settings

            Theme

            \ No newline at end of file diff --git a/frontend/docs/modules/utils.Api.html b/frontend/docs/modules/utils.Api.html index 5e789d477..ee59025ac 100644 --- a/frontend/docs/modules/utils.Api.html +++ b/frontend/docs/modules/utils.Api.html @@ -1,6 +1,7 @@ -Api | OSOC 3 - Frontend Documentation
            Options
            All
            • Public
            • Public/Protected
            • All
            Menu

            Index

            Functions

            • setBearerToken(value: null | string): void
            • -

              Set the default bearer token in the request headers

              -

              Parameters

              • value: null | string

              Returns void

            • validateRegistrationUrl(edition: string, uuid: string): Promise<boolean>
            • -

              Check if a registration url exists by sending a GET to it, -if it returns a 200 then we know the url is valid.

              +Api | OSOC 3 - Frontend Documentation
              Options
              All
              • Public
              • Public/Protected
              • All
              Menu

              Index

              Functions

              • setBearerToken(value: null | string): void
              • +

                Function to set the default bearer token in the request headers. +Passing null as the value will remove the header instead.

                +

                Parameters

                • value: null | string

                Returns void

              • validateRegistrationUrl(edition: string, uuid: string): Promise<boolean>
              • +

                Function to check if a registration url exists by sending a GET request, +if this returns a 200 then we know the url is valid.

                Parameters

                • edition: string
                • uuid: string

                Returns Promise<boolean>

              Legend

              • Namespace
              • Function
              • Interface

              Settings

              Theme

              \ No newline at end of file diff --git a/frontend/docs/modules/utils.LocalStorage.html b/frontend/docs/modules/utils.LocalStorage.html index b11f6bfe0..6b8c7bea5 100644 --- a/frontend/docs/modules/utils.LocalStorage.html +++ b/frontend/docs/modules/utils.LocalStorage.html @@ -1,6 +1,6 @@ -LocalStorage | OSOC 3 - Frontend Documentation
              Options
              All
              • Public
              • Public/Protected
              • All
              Menu

              Index

              Functions

              • getToken(): string | null
              • +LocalStorage | OSOC 3 - Frontend Documentation
                Options
                All
                • Public
                • Public/Protected
                • All
                Menu

                Index

                Functions

                • getToken(): string | null
                • Pull the user's token out of LocalStorage Returns null if there is no token in LocalStorage yet

                  -

                  Returns string | null

                • setToken(value: null | string): void
                • setToken(value: null | string): void

                Legend

                • Namespace
                • Function
                • Interface

                Settings

                Theme

                \ No newline at end of file diff --git a/frontend/docs/modules/views.Errors.html b/frontend/docs/modules/views.Errors.html index 43eb264e5..08f518a7f 100644 --- a/frontend/docs/modules/views.Errors.html +++ b/frontend/docs/modules/views.Errors.html @@ -1 +1 @@ -Errors | OSOC 3 - Frontend Documentation
                Options
                All
                • Public
                • Public/Protected
                • All
                Menu

                Index

                Functions

                • ForbiddenPage(): Element
                • NotFoundPage(): Element

                Legend

                • Namespace
                • Function
                • Interface

                Settings

                Theme

                \ No newline at end of file +Errors | OSOC 3 - Frontend Documentation
                Options
                All
                • Public
                • Public/Protected
                • All
                Menu

                Index

                Functions

                • ForbiddenPage(): Element
                • NotFoundPage(): Element

                Legend

                • Namespace
                • Function
                • Interface

                Settings

                Theme

                \ No newline at end of file diff --git a/frontend/docs/modules/views.html b/frontend/docs/modules/views.html index 0ecfe0dfe..e95ae9f7c 100644 --- a/frontend/docs/modules/views.html +++ b/frontend/docs/modules/views.html @@ -1 +1 @@ -views | OSOC 3 - Frontend Documentation
                Options
                All
                • Public
                • Public/Protected
                • All
                Menu

                Index

                Functions

                • LoginPage(): Element
                • PendingPage(): Element
                • ProjectsPage(): Element
                • RegisterPage(): Element
                • StudentsPage(): Element
                • UsersPage(): Element
                • VerifyingTokenPage(): Element

                Legend

                • Namespace
                • Function
                • Interface

                Settings

                Theme

                \ No newline at end of file +views | OSOC 3 - Frontend Documentation
                Options
                All
                • Public
                • Public/Protected
                • All
                Menu

                Index

                Functions

                • LoginPage(): Element
                • PendingPage(): Element
                • ProjectsPage(): Element
                • RegisterPage(): Element
                • StudentsPage(): Element
                • UsersPage(): Element
                • VerifyingTokenPage(): Element

                Legend

                • Namespace
                • Function
                • Interface

                Settings

                Theme

                \ No newline at end of file diff --git a/frontend/src/components/AdminRoute/AdminRoute.tsx b/frontend/src/components/AdminRoute/AdminRoute.tsx index 5f11c352d..f88399c49 100644 --- a/frontend/src/components/AdminRoute/AdminRoute.tsx +++ b/frontend/src/components/AdminRoute/AdminRoute.tsx @@ -3,9 +3,9 @@ import { useAuth } from "../../contexts/auth-context"; import { Role } from "../../data/enums"; /** - * React component for admin-only routes - * Goes to the [[LoginPage]] if not authenticated, and to the [[ForbiddenPage]] - * (status 403) if not admin + * React component for admin-only routes. + * Redirects to the [[LoginPage]] (status 401) if not authenticated, + * and to the [[ForbiddenPage]] (status 403) if not admin. * * Example usage: * ```ts diff --git a/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx b/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx index 835dca6c4..31a98312e 100644 --- a/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx +++ b/frontend/src/components/LoginComponents/InputFields/Email/Email.tsx @@ -1,7 +1,7 @@ import { Input } from "../styles"; /** - * Input field for email addresses + * Input field for email addresses. * @param email getter for the state of the email address * @param setEmail setter for the state of the email address */ diff --git a/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx b/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx index 94ee8b8f8..7af91b167 100644 --- a/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx +++ b/frontend/src/components/LoginComponents/InputFields/Password/Password.tsx @@ -1,7 +1,7 @@ import { Input } from "../styles"; /** - * Input field for passwords, authenticates when pressing the Enter key + * Input field for passwords, authenticates when pressing the Enter key. * @param password getter for the state of the password * @param setPassword setter for the state of the password * @param callLogIn callback that tries to authenticate the user diff --git a/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx b/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx index a5e7489b5..d8a92cf5f 100644 --- a/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx +++ b/frontend/src/components/RegisterComponents/InputFields/ConfirmPassword/ConfirmPassword.tsx @@ -1,7 +1,7 @@ import { Input } from "../styles"; /** - * Input field for passwords (confirmation), submits when pressing the Enter key + * Input field for passwords (confirmation), submits when pressing the Enter key. * @param confirmPassword getter for the state of the password * @param setConfirmPassword setter for the state of the password * @param callRegister callback that tries to register the user diff --git a/frontend/src/components/RegisterComponents/InputFields/Email/Email.tsx b/frontend/src/components/RegisterComponents/InputFields/Email/Email.tsx index 835dca6c4..31a98312e 100644 --- a/frontend/src/components/RegisterComponents/InputFields/Email/Email.tsx +++ b/frontend/src/components/RegisterComponents/InputFields/Email/Email.tsx @@ -1,7 +1,7 @@ import { Input } from "../styles"; /** - * Input field for email addresses + * Input field for email addresses. * @param email getter for the state of the email address * @param setEmail setter for the state of the email address */ diff --git a/frontend/src/components/RegisterComponents/InputFields/Name/Name.tsx b/frontend/src/components/RegisterComponents/InputFields/Name/Name.tsx index 72b9ffedf..911bb8dce 100644 --- a/frontend/src/components/RegisterComponents/InputFields/Name/Name.tsx +++ b/frontend/src/components/RegisterComponents/InputFields/Name/Name.tsx @@ -1,7 +1,7 @@ import { Input } from "../styles"; /** - * Input field for the user's name + * Input field for the user's name. * @param name getter for the state of the name * @param setName setter for the state of the name */ diff --git a/frontend/src/components/RegisterComponents/InputFields/Password/Password.tsx b/frontend/src/components/RegisterComponents/InputFields/Password/Password.tsx index b3f2802c2..fec45886d 100644 --- a/frontend/src/components/RegisterComponents/InputFields/Password/Password.tsx +++ b/frontend/src/components/RegisterComponents/InputFields/Password/Password.tsx @@ -1,7 +1,7 @@ import { Input } from "../styles"; /** - * Input field for passwords, authenticates when pressing the Enter key + * Input field for passwords, authenticates when pressing the Enter key. * @param password getter for the state of the password * @param setPassword setter for the state of the password * @param callLogIn callback that tries to authenticate the user diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx index cb374ae27..bdab830e2 100644 --- a/frontend/src/contexts/auth-context.tsx +++ b/frontend/src/contexts/auth-context.tsx @@ -3,6 +3,9 @@ import { Role } from "../data/enums"; import React, { useContext, ReactNode, useState } from "react"; import { getToken, setToken as setTokenInStorage } from "../utils/local-storage"; +/** + * Interface that holds the data stored in the AuthContext. + */ export interface AuthContextState { isLoggedIn: boolean | null; setIsLoggedIn: (value: boolean | null) => void; @@ -15,7 +18,9 @@ export interface AuthContextState { } /** - * Create a placeholder default value for the state + * Function to create a (placeholder) default value for the state. + * These values are never used, but React context hooks expect a default value + * so there is no way around it. */ function authDefaultState(): AuthContextState { return { @@ -33,7 +38,7 @@ function authDefaultState(): AuthContextState { const AuthContext = React.createContext(authDefaultState()); /** - * Custom React hook to use our authentication context + * Custom React hook to use our authentication context. */ export function useAuth(): AuthContextState { return useContext(AuthContext); @@ -41,10 +46,10 @@ export function useAuth(): AuthContextState { /** * Provider for auth that creates getters, setters, maintains state, and - * provides default values + * provides default values. * - * Not strictly necessary but keeps the main App clean by handling this - * code here instead + * This keeps the main [[App]] component code clean by handling this + * boilerplate here instead. */ export function AuthProvider({ children }: { children: ReactNode }) { const [isLoggedIn, setIsLoggedIn] = useState(null); diff --git a/frontend/src/data/enums/local-storage.ts b/frontend/src/data/enums/local-storage.ts index e1b55f9be..3b2765eb2 100644 --- a/frontend/src/data/enums/local-storage.ts +++ b/frontend/src/data/enums/local-storage.ts @@ -1,6 +1,9 @@ /** - * Keys in LocalStorage + * Enum for the keys in LocalStorage. */ export const enum StorageKey { + /** + * Bearer token used to authorize the user's requests in the backend. + */ BEARER_TOKEN = "bearerToken", } diff --git a/frontend/src/data/interfaces/users.ts b/frontend/src/data/interfaces/users.ts index 38b485eac..1c653263d 100644 --- a/frontend/src/data/interfaces/users.ts +++ b/frontend/src/data/interfaces/users.ts @@ -1,3 +1,8 @@ +/** + * Data about a user using the application. + * Contains a list of edition names so that we can quickly check if + * they have access to a route or not. + */ export interface User { userId: number; name: string; diff --git a/frontend/src/utils/api/api.ts b/frontend/src/utils/api/api.ts index de7a51812..2995a16e4 100644 --- a/frontend/src/utils/api/api.ts +++ b/frontend/src/utils/api/api.ts @@ -5,7 +5,8 @@ export const axiosInstance = axios.create(); axiosInstance.defaults.baseURL = BASE_URL; /** - * Set the default bearer token in the request headers + * Function to set the default bearer token in the request headers. + * Passing `null` as the value will remove the header instead. */ export function setBearerToken(value: string | null) { // Remove the header diff --git a/frontend/src/utils/api/auth.ts b/frontend/src/utils/api/auth.ts index 4a6638621..2d3712d43 100644 --- a/frontend/src/utils/api/auth.ts +++ b/frontend/src/utils/api/auth.ts @@ -3,8 +3,8 @@ import { axiosInstance } from "./api"; import { User } from "../../data/interfaces"; /** - * Check if a bearer token is valid - * @param token + * Check if a bearer token is valid. + * @param token the token to validate. */ export async function validateBearerToken(token: string | null): Promise { // No token stored -> can't validate anything @@ -30,8 +30,8 @@ export async function validateBearerToken(token: string | null): Promise { try { diff --git a/frontend/src/utils/api/login.ts b/frontend/src/utils/api/login.ts index 4fede8e34..bc2ae5570 100644 --- a/frontend/src/utils/api/login.ts +++ b/frontend/src/utils/api/login.ts @@ -11,6 +11,13 @@ interface LoginResponse { }; } +/** + * 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 email email entered + * @param password password entered + */ export async function logIn(auth: AuthContextState, email: string, password: string) { const payload = new FormData(); payload.append("username", email); From d1a7c448ba06d0a6dcb9e7e4e0d3e68f44533b9a Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 3 Apr 2022 16:43:08 +0200 Subject: [PATCH 219/536] add forgotten db.commit(); fixed remove_coach --- backend/src/database/crud/users.py | 17 ++++++----------- .../tests/test_database/test_crud/test_users.py | 13 ++++++++----- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index f89a01b7b..136593a17 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -53,6 +53,7 @@ def add_coach(db: Session, user_id: int, edition_name: str): user = db.query(User).where(User.user_id == user_id).one() edition = db.query(Edition).where(Edition.name == edition_name).one() user.editions.append(edition) + db.commit() def remove_coach(db: Session, user_id: int, edition_name: str): @@ -60,17 +61,11 @@ def remove_coach(db: Session, 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.execute(user_editions.delete(), {"user_id": user_id, "edition_id": edition.edition_id}) - - -def delete_user_as_coach(db: Session, edition_name: str, user_id: int): - """ - Add user as admin for the given edition if not already coach - """ - - user = db.query(User).where(User.user_id == user_id).one() - edition = db.query(Edition).where(Edition.name == edition_name).one() - user.editions.remove(edition) + db.query(user_editions)\ + .where(user_editions.c.user_id == user_id)\ + .where(user_editions.c.edition_id == edition.edition_id)\ + .delete() + db.commit() def get_all_requests(db: Session) -> list[CoachRequest]: diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index 549000694..a975a1395 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -124,8 +124,10 @@ def test_remove_coach(database_session: Session): """Test removing a user as coach""" # Create user - user = models.User(name="user1", admin=False) - database_session.add(user) + user1 = models.User(name="user1", admin=False) + database_session.add(user1) + user2 = models.User(name="user2", admin=False) + database_session.add(user2) # Create edition edition = models.Edition(year=1, name="ed1") @@ -135,11 +137,12 @@ def test_remove_coach(database_session: Session): # Create coach role database_session.execute(models.user_editions.insert(), [ - {"user_id": user.user_id, "edition_id": edition.edition_id} + {"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, user.user_id, edition.name) - assert len(database_session.query(user_editions).all()) == 0 + users_crud.remove_coach(database_session, user1.user_id, edition.name) + assert len(database_session.query(user_editions).all()) == 1 def test_get_all_requests(database_session: Session): From ee916aeff5f55f198b082af6f961d20761ed64ba Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 3 Apr 2022 16:44:00 +0200 Subject: [PATCH 220/536] Docstrings for utils --- frontend/src/utils/api/register.ts | 8 ++++++++ frontend/src/utils/local-storage/auth.ts | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/src/utils/api/register.ts b/frontend/src/utils/api/register.ts index 0314a2a5e..372df866d 100644 --- a/frontend/src/utils/api/register.ts +++ b/frontend/src/utils/api/register.ts @@ -8,6 +8,14 @@ interface RegisterFields { pw: string; } +/** + * Function to register a user in the backend. + * @param edition the name of the edition that the user is registering for + * @param email the email entered + * @param name the name entered + * @param uuid the uuid of the invitation link that was used + * @param password the password entered + */ export async function register( edition: string, email: string, diff --git a/frontend/src/utils/local-storage/auth.ts b/frontend/src/utils/local-storage/auth.ts index 6cb70be2f..86d75d3f6 100644 --- a/frontend/src/utils/local-storage/auth.ts +++ b/frontend/src/utils/local-storage/auth.ts @@ -1,7 +1,7 @@ import { StorageKey } from "../../data/enums"; /** - * Write the new value of a token into LocalStorage + * Function to set a new value for the bearer token in LocalStorage. */ export function setToken(value: string | null) { if (value === null) { @@ -12,8 +12,8 @@ export function setToken(value: string | null) { } /** - * Pull the user's token out of LocalStorage - * Returns null if there is no token in LocalStorage yet + * Function to pull the user's token out of LocalStorage. + * Returns `null` if there is no token in LocalStorage yet. */ export function getToken(): string | null { return localStorage.getItem(StorageKey.BEARER_TOKEN); From 5e397325c729901c92999f718da5a8e673e6134e Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 3 Apr 2022 17:19:40 +0200 Subject: [PATCH 221/536] Comments for pages --- frontend/src/views/LoginPage/LoginPage.tsx | 17 +++++++++-------- frontend/src/views/PendingPage/PendingPage.tsx | 8 +++++--- .../src/views/RegisterPage/RegisterPage.tsx | 8 +++++--- .../VerifyingTokenPage/VerifyingTokenPage.tsx | 4 ++++ .../errors/ForbiddenPage/ForbiddenPage.tsx | 9 ++++++--- .../views/errors/NotFoundPage/NotFoundPage.tsx | 7 ++++--- 6 files changed, 33 insertions(+), 20 deletions(-) diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index 3488b1a67..abde13a18 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -3,19 +3,22 @@ import { useNavigate } from "react-router-dom"; import { logIn } from "../../utils/api/login"; -import { WelcomeText, SocialButtons, Email, Password } from "../../components/LoginComponents"; +import { Email, Password, SocialButtons, WelcomeText } from "../../components/LoginComponents"; import { - LoginPageContainer, - LoginContainer, EmailLoginContainer, - VerticalDivider, - NoAccount, LoginButton, + LoginContainer, + LoginPageContainer, + NoAccount, + VerticalDivider, } from "./styles"; import { useAuth } from "../../contexts/auth-context"; -function LoginPage() { +/** + * Page where users can log in to the application. + */ +export default function LoginPage() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const authCtx = useAuth(); @@ -68,5 +71,3 @@ function LoginPage() {
                ); } - -export default LoginPage; diff --git a/frontend/src/views/PendingPage/PendingPage.tsx b/frontend/src/views/PendingPage/PendingPage.tsx index b9e937f43..a343cbdb0 100644 --- a/frontend/src/views/PendingPage/PendingPage.tsx +++ b/frontend/src/views/PendingPage/PendingPage.tsx @@ -8,7 +8,11 @@ import { PendingText, } from "./styles"; -function PendingPage() { +/** + * Page shown when your request to access an edition hasn't been accepted + * (or rejected) yet. + */ +export default function PendingPage() { return (
                @@ -29,5 +33,3 @@ function PendingPage() {
                ); } - -export default PendingPage; diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index 050cc49b1..761e28f05 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -16,7 +16,11 @@ import { import { RegisterFormContainer, Or, RegisterButton } from "./styles"; -function RegisterPage() { +/** + * 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 [validUuid, setUuid] = useState(false); const params = useParams(); const uuid = params.uuid; @@ -90,5 +94,3 @@ function RegisterPage() { ); } else return ; } - -export default RegisterPage; diff --git a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx index 6fae95c40..82b2dbbdf 100644 --- a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx +++ b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx @@ -5,6 +5,10 @@ import { validateBearerToken } from "../../utils/api/auth"; import { Role } from "../../data/enums"; import { useAuth } from "../../contexts/auth-context"; +/** + * Placeholder page shown while the bearer token found in LocalStorage is being verified. + * If the token is valid, redirects to the application. Otherwise, redirects to the [[LoginPage]]. + */ export default function VerifyingTokenPage() { const authContext = useAuth(); diff --git a/frontend/src/views/errors/ForbiddenPage/ForbiddenPage.tsx b/frontend/src/views/errors/ForbiddenPage/ForbiddenPage.tsx index 9eced5faf..106412a04 100644 --- a/frontend/src/views/errors/ForbiddenPage/ForbiddenPage.tsx +++ b/frontend/src/views/errors/ForbiddenPage/ForbiddenPage.tsx @@ -1,7 +1,12 @@ import React from "react"; import { ErrorContainer } from "../styles"; -function ForbiddenPage() { +/** + * Page shown to users when they try to access a resource they aren't + * authorized to. Examples include coaches performing admin actions, + * or coaches going to urls for editions they aren't part of. + */ +export default function ForbiddenPage() { return (

                Stop right there!

                @@ -9,5 +14,3 @@ function ForbiddenPage() {
                ); } - -export default ForbiddenPage; diff --git a/frontend/src/views/errors/NotFoundPage/NotFoundPage.tsx b/frontend/src/views/errors/NotFoundPage/NotFoundPage.tsx index d47a432ae..04a0c20fb 100644 --- a/frontend/src/views/errors/NotFoundPage/NotFoundPage.tsx +++ b/frontend/src/views/errors/NotFoundPage/NotFoundPage.tsx @@ -1,7 +1,10 @@ import React from "react"; import { ErrorContainer } from "../styles"; -function NotFoundPage() { +/** + * Page shown when going to a url for a page that doesn't exist. + */ +export default function NotFoundPage() { return (

                Oops! This is awkward...

                @@ -9,5 +12,3 @@ function NotFoundPage() {
                ); } - -export default NotFoundPage; From 1206aaee8ac27a144b22783aefcea33d53ade132 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 3 Apr 2022 17:22:07 +0200 Subject: [PATCH 222/536] Final comments, re-generate docs --- frontend/docs/enums/data.Enums.Role.html | 2 +- frontend/docs/enums/data.Enums.StorageKey.html | 2 +- .../docs/interfaces/contexts.AuthContextState.html | 2 +- frontend/docs/interfaces/data.Interfaces.User.html | 2 +- frontend/docs/modules/App.html | 5 ++++- frontend/docs/modules/Router.html | 5 ++++- .../docs/modules/components.LoginComponents.html | 8 ++++---- .../modules/components.RegisterComponents.html | 14 +++++++------- frontend/docs/modules/components.html | 10 +++++----- frontend/docs/modules/contexts.html | 2 +- frontend/docs/modules/utils.Api.html | 4 ++-- frontend/docs/modules/utils.LocalStorage.html | 10 +++++----- frontend/docs/modules/views.Errors.html | 8 +++++++- frontend/docs/modules/views.html | 13 ++++++++++++- frontend/src/App.tsx | 10 ++++++---- frontend/src/Router.tsx | 4 ++++ 16 files changed, 65 insertions(+), 36 deletions(-) diff --git a/frontend/docs/enums/data.Enums.Role.html b/frontend/docs/enums/data.Enums.Role.html index 63cd2b0b4..2c0e76c85 100644 --- a/frontend/docs/enums/data.Enums.Role.html +++ b/frontend/docs/enums/data.Enums.Role.html @@ -1,4 +1,4 @@ Role | OSOC 3 - Frontend Documentation
                Options
                All
                • Public
                • Public/Protected
                • All
                Menu

                Enum for the different levels of authority a user can have

                -

                Index

                Enumeration members

                Enumeration members

                ADMIN = 0
                COACH = 1

                Legend

                • Namespace
                • Function
                • Interface

                Settings

                Theme

                \ No newline at end of file +

            Index

            Enumeration members

            Enumeration members

            ADMIN = 0
            COACH = 1

            Legend

            • Namespace
            • Function
            • Interface

            Settings

            Theme

            \ No newline at end of file diff --git a/frontend/docs/enums/data.Enums.StorageKey.html b/frontend/docs/enums/data.Enums.StorageKey.html index c82d6495e..a4449093e 100644 --- a/frontend/docs/enums/data.Enums.StorageKey.html +++ b/frontend/docs/enums/data.Enums.StorageKey.html @@ -1,5 +1,5 @@ StorageKey | OSOC 3 - Frontend Documentation
            Options
            All
            • Public
            • Public/Protected
            • All
            Menu

            Enum for the keys in LocalStorage.

            -

            Index

            Enumeration members

            Enumeration members

            BEARER_TOKEN = "bearerToken"
            +

            Index

            Enumeration members

            Enumeration members

            BEARER_TOKEN = "bearerToken"

            Bearer token used to authorize the user's requests in the backend.

            Legend

            • Namespace
            • Function
            • Interface

            Settings

            Theme

            \ No newline at end of file diff --git a/frontend/docs/interfaces/contexts.AuthContextState.html b/frontend/docs/interfaces/contexts.AuthContextState.html index 8dbf7bb88..db218e95b 100644 --- a/frontend/docs/interfaces/contexts.AuthContextState.html +++ b/frontend/docs/interfaces/contexts.AuthContextState.html @@ -1,3 +1,3 @@ AuthContextState | OSOC 3 - Frontend Documentation
            Options
            All
            • Public
            • Public/Protected
            • All
            Menu

            Interface that holds the data stored in the AuthContext.

            -

            Hierarchy

            • AuthContextState

            Index

            Properties

            editions: number[]
            isLoggedIn: null | boolean
            role: null | Role
            token: null | string

            Methods

            • setEditions(value: number[]): void
            • setIsLoggedIn(value: null | boolean): void
            • setRole(value: null | Role): void
            • setToken(value: null | string): void

            Legend

            • Interface
            • Property
            • Method
            • Namespace
            • Function

            Settings

            Theme

            \ No newline at end of file +

          Hierarchy

          • AuthContextState

          Index

          Properties

          editions: number[]
          isLoggedIn: null | boolean
          role: null | Role
          token: null | string

          Methods

          • setEditions(value: number[]): void
          • setIsLoggedIn(value: null | boolean): void
          • setRole(value: null | Role): void
          • setToken(value: null | string): void

          Legend

          • Interface
          • Property
          • Method
          • Namespace
          • Function

          Settings

          Theme

          \ No newline at end of file diff --git a/frontend/docs/interfaces/data.Interfaces.User.html b/frontend/docs/interfaces/data.Interfaces.User.html index bf66f6bbc..8671eeca9 100644 --- a/frontend/docs/interfaces/data.Interfaces.User.html +++ b/frontend/docs/interfaces/data.Interfaces.User.html @@ -2,4 +2,4 @@

          Data about a user using the application. Contains a list of edition names so that we can quickly check if they have access to a route or not.

          -

        Hierarchy

        • User

        Index

        Properties

        admin: boolean
        editions: string[]
        name: string
        userId: number

        Legend

        • Namespace
        • Function
        • Interface
        • Property

        Settings

        Theme

        \ No newline at end of file +

      Hierarchy

      • User

      Index

      Properties

      admin: boolean
      editions: string[]
      name: string
      userId: number

      Legend

      • Namespace
      • Function
      • Interface
      • Property

      Settings

      Theme

      \ No newline at end of file diff --git a/frontend/docs/modules/App.html b/frontend/docs/modules/App.html index 70c121b16..c535e7b86 100644 --- a/frontend/docs/modules/App.html +++ b/frontend/docs/modules/App.html @@ -1 +1,4 @@ -App | OSOC 3 - Frontend Documentation
      Options
      All
      • Public
      • Public/Protected
      • All
      Menu

      Index

      Functions

      Functions

      • default(): Element

      Legend

      • Namespace
      • Function
      • Interface

      Settings

      Theme

      \ No newline at end of file +App | OSOC 3 - Frontend Documentation
      Options
      All
      • Public
      • Public/Protected
      • All
      Menu

      Index

      Functions

      Functions

      • default(): Element

      Legend

      • Namespace
      • Function
      • Interface

      Settings

      Theme

      \ No newline at end of file diff --git a/frontend/docs/modules/Router.html b/frontend/docs/modules/Router.html index dfd049368..1cf66e604 100644 --- a/frontend/docs/modules/Router.html +++ b/frontend/docs/modules/Router.html @@ -1 +1,4 @@ -Router | OSOC 3 - Frontend Documentation
      Options
      All
      • Public
      • Public/Protected
      • All
      Menu

      Index

      Functions

      Functions

      • default(): Element

      Legend

      • Namespace
      • Function
      • Interface

      Settings

      Theme

      \ No newline at end of file +Router | OSOC 3 - Frontend Documentation
      Options
      All
      • Public
      • Public/Protected
      • All
      Menu

      Index

      Functions

      Functions

      • default(): Element
      • +

        Router component to render different pages depending on the current url. Renders +the VerifyingTokenPage if the bearer token is still being validated.

        +

        Returns Element

      Legend

      • Namespace
      • Function
      • Interface

      Settings

      Theme

      \ No newline at end of file diff --git a/frontend/docs/modules/components.LoginComponents.html b/frontend/docs/modules/components.LoginComponents.html index 96385af66..b8b12739f 100644 --- a/frontend/docs/modules/components.LoginComponents.html +++ b/frontend/docs/modules/components.LoginComponents.html @@ -1,9 +1,9 @@ -LoginComponents | OSOC 3 - Frontend Documentation
      Options
      All
      • Public
      • Public/Protected
      • All
      Menu

      Index

      Functions

      • Email(__namedParameters: { email: string; setEmail: any }): Element
      • +LoginComponents | OSOC 3 - Frontend Documentation
        Options
        All
        • Public
        • Public/Protected
        • All
        Menu

        Index

        Functions

        • Email(__namedParameters: { email: string; setEmail: any }): Element
        • Password(__namedParameters: { password: string; callLogIn: any; setPassword: any }): Element
        • Password(__namedParameters: { password: string; callLogIn: any; setPassword: any }): Element
        • SocialButtons(): Element
        • SocialButtons(): Element
        • WelcomeText(): Element
        • WelcomeText(): Element

        Legend

        • Namespace
        • Function
        • Interface

        Settings

        Theme

        \ No newline at end of file diff --git a/frontend/docs/modules/components.RegisterComponents.html b/frontend/docs/modules/components.RegisterComponents.html index dddd0e1af..3524a2733 100644 --- a/frontend/docs/modules/components.RegisterComponents.html +++ b/frontend/docs/modules/components.RegisterComponents.html @@ -1,15 +1,15 @@ -RegisterComponents | OSOC 3 - Frontend Documentation
        Options
        All
        • Public
        • Public/Protected
        • All
        Menu

        Index

        Functions

        • BadInviteLink(): Element
        • SocialButtons(): Element

        Legend

        • Namespace
        • Function
        • Interface

        Settings

        Theme

        \ No newline at end of file diff --git a/frontend/docs/modules/components.html b/frontend/docs/modules/components.html index 2bfe959da..26769a478 100644 --- a/frontend/docs/modules/components.html +++ b/frontend/docs/modules/components.html @@ -1,23 +1,23 @@ -components | OSOC 3 - Frontend Documentation
        Options
        All
        • Public
        • Public/Protected
        • All
        Menu

        Index

        Functions

        • AdminRoute(): Element
        • +components | OSOC 3 - Frontend Documentation
          Options
          All
          • Public
          • Public/Protected
          • All
          Menu

          Index

          Functions

          • AdminRoute(): Element
          • React component for admin-only routes. Redirects to the LoginPage (status 401) if not authenticated, and to the ForbiddenPage (status 403) if not admin.

            Example usage:

            <Route path={"/path"} element={<AdminRoute />}>
            // These routes will only render if the user is an admin
            <Route path={"/"} />
            <Route path={"/child"} />
            </Route>
            -

            Returns Element

          • Footer(): Element
          • Footer(): Element
          • Footer placed at the bottom of the site, containing various links related to the application or our code.

            The footer is only displayed when signed in.

            -

            Returns Element

          • NavBar(): Element
          • NavBar(): Element
          • NavBar displayed at the top of the page. Links are hidden if the user is not authorized to see them.

            -

            Returns Element

          • OSOCLetters(): Element
          • OSOCLetters(): Element
          • Animated OSOC-letters, inspired by the ones found on the OSOC website.

            Note: This component is currently not in use because the positioning of the letters causes issues. We have given priority to other parts of the application.

            -

            Returns Element

          • PrivateRoute(): Element
          • PrivateRoute(): Element
          • React component that redirects to the LoginPage if not authenticated when trying to visit a route.

            Example usage:

            diff --git a/frontend/docs/modules/contexts.html b/frontend/docs/modules/contexts.html index e77922072..312b17b06 100644 --- a/frontend/docs/modules/contexts.html +++ b/frontend/docs/modules/contexts.html @@ -1,4 +1,4 @@ -contexts | OSOC 3 - Frontend Documentation
            Options
            All
            • Public
            • Public/Protected
            • All
            Menu

            Index

            Interfaces

            Functions

            Functions

            • AuthProvider(__namedParameters: { children: ReactNode }): Element
            • +contexts | OSOC 3 - Frontend Documentation
              Options
              All
              • Public
              • Public/Protected
              • All
              Menu

              Index

              Interfaces

              Functions

              Functions

              • AuthProvider(__namedParameters: { children: ReactNode }): Element
              • Provider for auth that creates getters, setters, maintains state, and provides default values.

                This keeps the main App component code clean by handling this diff --git a/frontend/docs/modules/utils.Api.html b/frontend/docs/modules/utils.Api.html index ee59025ac..374ff7c4d 100644 --- a/frontend/docs/modules/utils.Api.html +++ b/frontend/docs/modules/utils.Api.html @@ -1,7 +1,7 @@ -Api | OSOC 3 - Frontend Documentation

                Options
                All
                • Public
                • Public/Protected
                • All
                Menu

                Index

                Functions

                • setBearerToken(value: null | string): void
                • +Api | OSOC 3 - Frontend Documentation
                  Options
                  All
                  • Public
                  • Public/Protected
                  • All
                  Menu

                  Index

                  Functions

                  • setBearerToken(value: null | string): void
                  • Function to set the default bearer token in the request headers. Passing null as the value will remove the header instead.

                    -

                    Parameters

                    • value: null | string

                    Returns void

                  • validateRegistrationUrl(edition: string, uuid: string): Promise<boolean>
                  • validateRegistrationUrl(edition: string, uuid: string): Promise<boolean>
                  • Function to check if a registration url exists by sending a GET request, if this returns a 200 then we know the url is valid.

                    Parameters

                    • edition: string
                    • uuid: string

                    Returns Promise<boolean>

                  Legend

                  • Namespace
                  • Function
                  • Interface

                  Settings

                  Theme

                  \ No newline at end of file diff --git a/frontend/docs/modules/utils.LocalStorage.html b/frontend/docs/modules/utils.LocalStorage.html index 6b8c7bea5..de2ce1baf 100644 --- a/frontend/docs/modules/utils.LocalStorage.html +++ b/frontend/docs/modules/utils.LocalStorage.html @@ -1,6 +1,6 @@ -LocalStorage | OSOC 3 - Frontend Documentation
                  Options
                  All
                  • Public
                  • Public/Protected
                  • All
                  Menu

                  Index

                  Functions

                  • getToken(): string | null
                  • -

                    Pull the user's token out of LocalStorage -Returns null if there is no token in LocalStorage yet

                    -

                    Returns string | null

                  • setToken(value: null | string): void
                  • -

                    Write the new value of a token into LocalStorage

                    +LocalStorage | OSOC 3 - Frontend Documentation
                    Options
                    All
                    • Public
                    • Public/Protected
                    • All
                    Menu

                    Index

                    Functions

                    • getToken(): string | null
                    • +

                      Function to pull the user's token out of LocalStorage. +Returns null if there is no token in LocalStorage yet.

                      +

                      Returns string | null

                    • setToken(value: null | string): void
                    • +

                      Function to set a new value for the bearer token in LocalStorage.

                      Parameters

                      • value: null | string

                      Returns void

                    Legend

                    • Namespace
                    • Function
                    • Interface

                    Settings

                    Theme

                    \ No newline at end of file diff --git a/frontend/docs/modules/views.Errors.html b/frontend/docs/modules/views.Errors.html index 08f518a7f..bdf65408e 100644 --- a/frontend/docs/modules/views.Errors.html +++ b/frontend/docs/modules/views.Errors.html @@ -1 +1,7 @@ -Errors | OSOC 3 - Frontend Documentation
                    Options
                    All
                    • Public
                    • Public/Protected
                    • All
                    Menu

                    Index

                    Functions

                    • ForbiddenPage(): Element
                    • NotFoundPage(): Element

                    Legend

                    • Namespace
                    • Function
                    • Interface

                    Settings

                    Theme

                    \ No newline at end of file +Errors | OSOC 3 - Frontend Documentation
                    Options
                    All
                    • Public
                    • Public/Protected
                    • All
                    Menu

                    Index

                    Functions

                    • ForbiddenPage(): Element
                    • +

                      Page shown to users when they try to access a resource they aren't +authorized to. Examples include coaches performing admin actions, +or coaches going to urls for editions they aren't part of.

                      +

                      Returns Element

                    • NotFoundPage(): Element

                    Legend

                    • Namespace
                    • Function
                    • Interface

                    Settings

                    Theme

                    \ No newline at end of file diff --git a/frontend/docs/modules/views.html b/frontend/docs/modules/views.html index e95ae9f7c..4d61bcd40 100644 --- a/frontend/docs/modules/views.html +++ b/frontend/docs/modules/views.html @@ -1 +1,12 @@ -views | OSOC 3 - Frontend Documentation
                    Options
                    All
                    • Public
                    • Public/Protected
                    • All
                    Menu

                    Index

                    Functions

                    • LoginPage(): Element
                    • PendingPage(): Element
                    • ProjectsPage(): Element
                    • RegisterPage(): Element
                    • StudentsPage(): Element
                    • UsersPage(): Element
                    • VerifyingTokenPage(): Element

                    Legend

                    • Namespace
                    • Function
                    • Interface

                    Settings

                    Theme

                    \ No newline at end of file +views | OSOC 3 - Frontend Documentation
                    Options
                    All
                    • Public
                    • Public/Protected
                    • All
                    Menu

                    Index

                    Functions

                    • LoginPage(): Element
                    • PendingPage(): Element
                    • ProjectsPage(): Element
                    • RegisterPage(): Element
                    • StudentsPage(): Element
                    • UsersPage(): Element
                    • VerifyingTokenPage(): Element

                    Legend

                    • Namespace
                    • Function
                    • Interface

                    Settings

                    Theme

                    \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 40512d14d..cf4a170eb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,13 +4,15 @@ import "./App.css"; import Router from "./Router"; import { AuthProvider } from "./contexts"; -function App() { +/** + * Main application component. Wraps the [[Router]] in an [[AuthProvider]] so that + * the [[AuthContextState]] is available throughout the entire application. + */ +export default function App() { return ( - // AuthContext should be visible in the entire application + // AuthContext should be available in the entire application ); } - -export default App; diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 828409fa2..8bb4d2dd7 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -16,6 +16,10 @@ import AdminRoute from "./components/AdminRoute"; import { NotFoundPage } from "./views/errors"; import ForbiddenPage from "./views/errors/ForbiddenPage"; +/** + * Router component to render different pages depending on the current url. Renders + * the [[VerifyingTokenPage]] if the bearer token is still being validated. + */ export default function Router() { const { isLoggedIn } = useAuth(); From 3454d351b3530a0e4c40b6840b127115d57e4378 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 3 Apr 2022 21:46:01 +0200 Subject: [PATCH 223/536] Put components in components-folder --- frontend/docs/assets/search.js | 2 +- frontend/docs/enums/data.Enums.Role.html | 2 +- .../docs/enums/data.Enums.StorageKey.html | 2 +- .../interfaces/contexts.AuthContextState.html | 2 +- .../docs/interfaces/data.Interfaces.User.html | 2 +- frontend/docs/modules/App.html | 2 +- frontend/docs/modules/Router.html | 2 +- .../modules/components.LoginComponents.html | 10 +- .../components.RegisterComponents.html | 14 +- frontend/docs/modules/components.html | 12 +- frontend/docs/modules/contexts.html | 2 +- frontend/docs/modules/utils.Api.html | 6 +- frontend/docs/modules/utils.LocalStorage.html | 4 +- frontend/docs/modules/views.Errors.html | 4 +- frontend/docs/modules/views.html | 10 +- .../UsersComponents/Coaches/Coaches.tsx | 79 +++++ .../Coaches/CoachesComponents/AddCoach.tsx | 79 +++++ .../Coaches/CoachesComponents/CoachList.tsx | 54 ++++ .../CoachesComponents/CoachListItem.tsx | 19 ++ .../Coaches/CoachesComponents/RemoveCoach.tsx | 82 +++++ .../Coaches/CoachesComponents/index.ts | 4 + .../UsersComponents/Coaches/index.ts | 2 + .../UsersComponents}/Coaches/styles.ts | 0 .../InviteUser/InviteUser.css} | 0 .../InviteUser/InviteUser.tsx | 52 ++- .../InviteUserComponents/ButtonsDiv.tsx | 17 + .../InviteUserComponents/ErrorDiv.tsx | 10 + .../InviteUserComponents/LinkDiv.tsx | 10 + .../InviteUser/InviteUserComponents/index.ts | 3 + .../UsersComponents/InviteUser/index.ts | 2 + .../UsersComponents}/InviteUser/styles.ts | 10 + .../PendingRequests/PendingRequests.tsx | 68 ++++ .../AcceptReject.tsx | 12 + .../RequestFilter.tsx | 16 + .../PendingRequestsComponents/RequestList.tsx | 46 +++ .../RequestListItem.tsx | 15 + .../RequestsHeader.tsx | 19 ++ .../PendingRequestsComponents/index.ts | 5 + .../UsersComponents/PendingRequests/index.ts | 2 + .../PendingRequests/styles.ts | 0 .../src/components/UsersComponents/index.ts | 3 + frontend/src/components/index.ts | 1 + frontend/src/utils/api/index.ts | 1 + frontend/src/utils/api/users/admins.ts | 25 +- frontend/src/utils/api/users/coaches.ts | 28 +- frontend/src/utils/api/users/index.ts | 4 + frontend/src/utils/api/users/requests.ts | 18 ++ frontend/src/utils/api/users/users.ts | 44 +-- .../src/views/AdminsPage/Admins/Admins.tsx | 6 +- .../src/views/UsersPage/Coaches/Coaches.tsx | 302 ------------------ frontend/src/views/UsersPage/Coaches/index.ts | 1 - .../src/views/UsersPage/InviteUser/index.ts | 1 - .../PendingRequests/PendingRequests.tsx | 171 ---------- .../views/UsersPage/PendingRequests/index.ts | 1 - frontend/src/views/UsersPage/UsersPage.tsx | 9 +- 55 files changed, 711 insertions(+), 586 deletions(-) create mode 100644 frontend/src/components/UsersComponents/Coaches/Coaches.tsx create mode 100644 frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx create mode 100644 frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx create mode 100644 frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx create mode 100644 frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx create mode 100644 frontend/src/components/UsersComponents/Coaches/CoachesComponents/index.ts create mode 100644 frontend/src/components/UsersComponents/Coaches/index.ts rename frontend/src/{views/UsersPage => components/UsersComponents}/Coaches/styles.ts (100%) rename frontend/src/{views/UsersPage/InviteUser/InviteUsers.css => components/UsersComponents/InviteUser/InviteUser.css} (100%) rename frontend/src/{views/UsersPage => components/UsersComponents}/InviteUser/InviteUser.tsx (53%) create mode 100644 frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ButtonsDiv.tsx create mode 100644 frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ErrorDiv.tsx create mode 100644 frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/LinkDiv.tsx create mode 100644 frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/index.ts create mode 100644 frontend/src/components/UsersComponents/InviteUser/index.ts rename frontend/src/{views/UsersPage => components/UsersComponents}/InviteUser/styles.ts (86%) create mode 100644 frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx create mode 100644 frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/AcceptReject.tsx create mode 100644 frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestFilter.tsx create mode 100644 frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx create mode 100644 frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestListItem.tsx create mode 100644 frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestsHeader.tsx create mode 100644 frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/index.ts create mode 100644 frontend/src/components/UsersComponents/PendingRequests/index.ts rename frontend/src/{views/UsersPage => components/UsersComponents}/PendingRequests/styles.ts (100%) create mode 100644 frontend/src/components/UsersComponents/index.ts create mode 100644 frontend/src/utils/api/users/index.ts delete mode 100644 frontend/src/views/UsersPage/Coaches/Coaches.tsx delete mode 100644 frontend/src/views/UsersPage/Coaches/index.ts delete mode 100644 frontend/src/views/UsersPage/InviteUser/index.ts delete mode 100644 frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx delete mode 100644 frontend/src/views/UsersPage/PendingRequests/index.ts diff --git a/frontend/docs/assets/search.js b/frontend/docs/assets/search.js index 3c0bcbf41..d01761199 100644 --- a/frontend/docs/assets/search.js +++ b/frontend/docs/assets/search.js @@ -1 +1 @@ -window.searchData = JSON.parse("{\"kinds\":{\"2\":\"Module\",\"4\":\"Namespace\",\"8\":\"Enumeration\",\"16\":\"Enumeration member\",\"64\":\"Function\",\"256\":\"Interface\",\"1024\":\"Property\",\"2048\":\"Method\"},\"rows\":[{\"id\":0,\"kind\":2,\"name\":\"App\",\"url\":\"modules/App.html\",\"classes\":\"tsd-kind-module\"},{\"id\":1,\"kind\":64,\"name\":\"default\",\"url\":\"modules/App.html#default\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"App\"},{\"id\":2,\"kind\":2,\"name\":\"Router\",\"url\":\"modules/Router.html\",\"classes\":\"tsd-kind-module\"},{\"id\":3,\"kind\":64,\"name\":\"default\",\"url\":\"modules/Router.html#default\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"Router\"},{\"id\":4,\"kind\":2,\"name\":\"components\",\"url\":\"modules/components.html\",\"classes\":\"tsd-kind-module\"},{\"id\":5,\"kind\":2,\"name\":\"contexts\",\"url\":\"modules/contexts.html\",\"classes\":\"tsd-kind-module\"},{\"id\":6,\"kind\":2,\"name\":\"data\",\"url\":\"modules/data.html\",\"classes\":\"tsd-kind-module\"},{\"id\":7,\"kind\":2,\"name\":\"utils\",\"url\":\"modules/utils.html\",\"classes\":\"tsd-kind-module\"},{\"id\":8,\"kind\":2,\"name\":\"views\",\"url\":\"modules/views.html\",\"classes\":\"tsd-kind-module\"},{\"id\":9,\"kind\":64,\"name\":\"AdminRoute\",\"url\":\"modules/components.html#AdminRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":10,\"kind\":64,\"name\":\"Footer\",\"url\":\"modules/components.html#Footer\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":11,\"kind\":4,\"name\":\"LoginComponents\",\"url\":\"modules/components.LoginComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":12,\"kind\":64,\"name\":\"WelcomeText\",\"url\":\"modules/components.LoginComponents.html#WelcomeText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":13,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/components.LoginComponents.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":14,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/components.LoginComponents.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":15,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/components.LoginComponents.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":16,\"kind\":64,\"name\":\"NavBar\",\"url\":\"modules/components.html#NavBar\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":17,\"kind\":64,\"name\":\"OSOCLetters\",\"url\":\"modules/components.html#OSOCLetters\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":18,\"kind\":64,\"name\":\"PrivateRoute\",\"url\":\"modules/components.html#PrivateRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":19,\"kind\":4,\"name\":\"RegisterComponents\",\"url\":\"modules/components.RegisterComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":20,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/components.RegisterComponents.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":21,\"kind\":64,\"name\":\"Name\",\"url\":\"modules/components.RegisterComponents.html#Name\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":22,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/components.RegisterComponents.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":23,\"kind\":64,\"name\":\"ConfirmPassword\",\"url\":\"modules/components.RegisterComponents.html#ConfirmPassword\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":24,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/components.RegisterComponents.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":25,\"kind\":64,\"name\":\"InfoText\",\"url\":\"modules/components.RegisterComponents.html#InfoText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":26,\"kind\":64,\"name\":\"BadInviteLink\",\"url\":\"modules/components.RegisterComponents.html#BadInviteLink\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":27,\"kind\":256,\"name\":\"AuthContextState\",\"url\":\"interfaces/contexts.AuthContextState.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-module\",\"parent\":\"contexts\"},{\"id\":28,\"kind\":1024,\"name\":\"isLoggedIn\",\"url\":\"interfaces/contexts.AuthContextState.html#isLoggedIn\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":29,\"kind\":2048,\"name\":\"setIsLoggedIn\",\"url\":\"interfaces/contexts.AuthContextState.html#setIsLoggedIn\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":30,\"kind\":1024,\"name\":\"role\",\"url\":\"interfaces/contexts.AuthContextState.html#role\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":31,\"kind\":2048,\"name\":\"setRole\",\"url\":\"interfaces/contexts.AuthContextState.html#setRole\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":32,\"kind\":1024,\"name\":\"token\",\"url\":\"interfaces/contexts.AuthContextState.html#token\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":33,\"kind\":2048,\"name\":\"setToken\",\"url\":\"interfaces/contexts.AuthContextState.html#setToken\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":34,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/contexts.AuthContextState.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":35,\"kind\":2048,\"name\":\"setEditions\",\"url\":\"interfaces/contexts.AuthContextState.html#setEditions\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":36,\"kind\":64,\"name\":\"AuthProvider\",\"url\":\"modules/contexts.html#AuthProvider\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"contexts\"},{\"id\":37,\"kind\":4,\"name\":\"Enums\",\"url\":\"modules/data.Enums.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"data\"},{\"id\":38,\"kind\":8,\"name\":\"StorageKey\",\"url\":\"enums/data.Enums.StorageKey.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"data.Enums\"},{\"id\":39,\"kind\":16,\"name\":\"BEARER_TOKEN\",\"url\":\"enums/data.Enums.StorageKey.html#BEARER_TOKEN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.StorageKey\"},{\"id\":40,\"kind\":8,\"name\":\"Role\",\"url\":\"enums/data.Enums.Role.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"data.Enums\"},{\"id\":41,\"kind\":16,\"name\":\"ADMIN\",\"url\":\"enums/data.Enums.Role.html#ADMIN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.Role\"},{\"id\":42,\"kind\":16,\"name\":\"COACH\",\"url\":\"enums/data.Enums.Role.html#COACH\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.Role\"},{\"id\":43,\"kind\":4,\"name\":\"Interfaces\",\"url\":\"modules/data.Interfaces.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"data\"},{\"id\":44,\"kind\":256,\"name\":\"User\",\"url\":\"interfaces/data.Interfaces.User.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"data.Interfaces\"},{\"id\":45,\"kind\":1024,\"name\":\"userId\",\"url\":\"interfaces/data.Interfaces.User.html#userId\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":46,\"kind\":1024,\"name\":\"name\",\"url\":\"interfaces/data.Interfaces.User.html#name\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":47,\"kind\":1024,\"name\":\"admin\",\"url\":\"interfaces/data.Interfaces.User.html#admin\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":48,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/data.Interfaces.User.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":49,\"kind\":4,\"name\":\"Api\",\"url\":\"modules/utils.Api.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"utils\"},{\"id\":50,\"kind\":64,\"name\":\"validateRegistrationUrl\",\"url\":\"modules/utils.Api.html#validateRegistrationUrl\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api\"},{\"id\":51,\"kind\":64,\"name\":\"setBearerToken\",\"url\":\"modules/utils.Api.html#setBearerToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api\"},{\"id\":52,\"kind\":4,\"name\":\"LocalStorage\",\"url\":\"modules/utils.LocalStorage.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"utils\"},{\"id\":53,\"kind\":64,\"name\":\"getToken\",\"url\":\"modules/utils.LocalStorage.html#getToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.LocalStorage\"},{\"id\":54,\"kind\":64,\"name\":\"setToken\",\"url\":\"modules/utils.LocalStorage.html#setToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.LocalStorage\"},{\"id\":55,\"kind\":4,\"name\":\"Errors\",\"url\":\"modules/views.Errors.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":56,\"kind\":64,\"name\":\"ForbiddenPage\",\"url\":\"modules/views.Errors.html#ForbiddenPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"views.Errors\"},{\"id\":57,\"kind\":64,\"name\":\"NotFoundPage\",\"url\":\"modules/views.Errors.html#NotFoundPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"views.Errors\"},{\"id\":58,\"kind\":64,\"name\":\"LoginPage\",\"url\":\"modules/views.html#LoginPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":59,\"kind\":64,\"name\":\"PendingPage\",\"url\":\"modules/views.html#PendingPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":60,\"kind\":64,\"name\":\"ProjectsPage\",\"url\":\"modules/views.html#ProjectsPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":61,\"kind\":64,\"name\":\"RegisterPage\",\"url\":\"modules/views.html#RegisterPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":62,\"kind\":64,\"name\":\"StudentsPage\",\"url\":\"modules/views.html#StudentsPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":63,\"kind\":64,\"name\":\"UsersPage\",\"url\":\"modules/views.html#UsersPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":64,\"kind\":64,\"name\":\"VerifyingTokenPage\",\"url\":\"modules/views.html#VerifyingTokenPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"parent\"],\"fieldVectors\":[[\"name/0\",[0,32.734]],[\"parent/0\",[]],[\"name/1\",[1,32.734]],[\"parent/1\",[0,3.119]],[\"name/2\",[2,32.734]],[\"parent/2\",[]],[\"name/3\",[1,32.734]],[\"parent/3\",[2,3.119]],[\"name/4\",[3,20.496]],[\"parent/4\",[]],[\"name/5\",[4,29.369]],[\"parent/5\",[]],[\"name/6\",[5,29.369]],[\"parent/6\",[]],[\"name/7\",[6,29.369]],[\"parent/7\",[]],[\"name/8\",[7,19.384]],[\"parent/8\",[]],[\"name/9\",[8,37.842]],[\"parent/9\",[3,1.953]],[\"name/10\",[9,37.842]],[\"parent/10\",[3,1.953]],[\"name/11\",[10,37.842]],[\"parent/11\",[3,1.953]],[\"name/12\",[11,37.842]],[\"parent/12\",[12,2.559]],[\"name/13\",[13,32.734]],[\"parent/13\",[12,2.559]],[\"name/14\",[14,32.734]],[\"parent/14\",[12,2.559]],[\"name/15\",[15,32.734]],[\"parent/15\",[12,2.559]],[\"name/16\",[16,37.842]],[\"parent/16\",[3,1.953]],[\"name/17\",[17,37.842]],[\"parent/17\",[3,1.953]],[\"name/18\",[18,37.842]],[\"parent/18\",[3,1.953]],[\"name/19\",[19,37.842]],[\"parent/19\",[3,1.953]],[\"name/20\",[15,32.734]],[\"parent/20\",[20,2.072]],[\"name/21\",[21,32.734]],[\"parent/21\",[20,2.072]],[\"name/22\",[14,32.734]],[\"parent/22\",[20,2.072]],[\"name/23\",[22,37.842]],[\"parent/23\",[20,2.072]],[\"name/24\",[13,32.734]],[\"parent/24\",[20,2.072]],[\"name/25\",[23,37.842]],[\"parent/25\",[20,2.072]],[\"name/26\",[24,37.842]],[\"parent/26\",[20,2.072]],[\"name/27\",[25,37.842]],[\"parent/27\",[4,2.799]],[\"name/28\",[26,37.842]],[\"parent/28\",[27,1.953]],[\"name/29\",[28,37.842]],[\"parent/29\",[27,1.953]],[\"name/30\",[29,32.734]],[\"parent/30\",[27,1.953]],[\"name/31\",[30,37.842]],[\"parent/31\",[27,1.953]],[\"name/32\",[31,37.842]],[\"parent/32\",[27,1.953]],[\"name/33\",[32,32.734]],[\"parent/33\",[27,1.953]],[\"name/34\",[33,32.734]],[\"parent/34\",[27,1.953]],[\"name/35\",[34,37.842]],[\"parent/35\",[27,1.953]],[\"name/36\",[35,37.842]],[\"parent/36\",[4,2.799]],[\"name/37\",[36,37.842]],[\"parent/37\",[5,2.799]],[\"name/38\",[37,37.842]],[\"parent/38\",[38,3.119]],[\"name/39\",[39,37.842]],[\"parent/39\",[40,3.606]],[\"name/40\",[29,32.734]],[\"parent/40\",[38,3.119]],[\"name/41\",[41,32.734]],[\"parent/41\",[42,3.119]],[\"name/42\",[43,37.842]],[\"parent/42\",[42,3.119]],[\"name/43\",[44,37.842]],[\"parent/43\",[5,2.799]],[\"name/44\",[45,37.842]],[\"parent/44\",[46,3.606]],[\"name/45\",[47,37.842]],[\"parent/45\",[48,2.559]],[\"name/46\",[21,32.734]],[\"parent/46\",[48,2.559]],[\"name/47\",[41,32.734]],[\"parent/47\",[48,2.559]],[\"name/48\",[33,32.734]],[\"parent/48\",[48,2.559]],[\"name/49\",[49,37.842]],[\"parent/49\",[6,2.799]],[\"name/50\",[50,37.842]],[\"parent/50\",[51,3.119]],[\"name/51\",[52,37.842]],[\"parent/51\",[51,3.119]],[\"name/52\",[53,37.842]],[\"parent/52\",[6,2.799]],[\"name/53\",[54,37.842]],[\"parent/53\",[55,3.119]],[\"name/54\",[32,32.734]],[\"parent/54\",[55,3.119]],[\"name/55\",[56,37.842]],[\"parent/55\",[7,1.847]],[\"name/56\",[57,37.842]],[\"parent/56\",[58,3.119]],[\"name/57\",[59,37.842]],[\"parent/57\",[58,3.119]],[\"name/58\",[60,37.842]],[\"parent/58\",[7,1.847]],[\"name/59\",[61,37.842]],[\"parent/59\",[7,1.847]],[\"name/60\",[62,37.842]],[\"parent/60\",[7,1.847]],[\"name/61\",[63,37.842]],[\"parent/61\",[7,1.847]],[\"name/62\",[64,37.842]],[\"parent/62\",[7,1.847]],[\"name/63\",[65,37.842]],[\"parent/63\",[7,1.847]],[\"name/64\",[66,37.842]],[\"parent/64\",[7,1.847]]],\"invertedIndex\":[[\"admin\",{\"_index\":41,\"name\":{\"41\":{},\"47\":{}},\"parent\":{}}],[\"adminroute\",{\"_index\":8,\"name\":{\"9\":{}},\"parent\":{}}],[\"api\",{\"_index\":49,\"name\":{\"49\":{}},\"parent\":{}}],[\"app\",{\"_index\":0,\"name\":{\"0\":{}},\"parent\":{\"1\":{}}}],[\"authcontextstate\",{\"_index\":25,\"name\":{\"27\":{}},\"parent\":{}}],[\"authprovider\",{\"_index\":35,\"name\":{\"36\":{}},\"parent\":{}}],[\"badinvitelink\",{\"_index\":24,\"name\":{\"26\":{}},\"parent\":{}}],[\"bearer_token\",{\"_index\":39,\"name\":{\"39\":{}},\"parent\":{}}],[\"coach\",{\"_index\":43,\"name\":{\"42\":{}},\"parent\":{}}],[\"components\",{\"_index\":3,\"name\":{\"4\":{}},\"parent\":{\"9\":{},\"10\":{},\"11\":{},\"16\":{},\"17\":{},\"18\":{},\"19\":{}}}],[\"components.logincomponents\",{\"_index\":12,\"name\":{},\"parent\":{\"12\":{},\"13\":{},\"14\":{},\"15\":{}}}],[\"components.registercomponents\",{\"_index\":20,\"name\":{},\"parent\":{\"20\":{},\"21\":{},\"22\":{},\"23\":{},\"24\":{},\"25\":{},\"26\":{}}}],[\"confirmpassword\",{\"_index\":22,\"name\":{\"23\":{}},\"parent\":{}}],[\"contexts\",{\"_index\":4,\"name\":{\"5\":{}},\"parent\":{\"27\":{},\"36\":{}}}],[\"contexts.authcontextstate\",{\"_index\":27,\"name\":{},\"parent\":{\"28\":{},\"29\":{},\"30\":{},\"31\":{},\"32\":{},\"33\":{},\"34\":{},\"35\":{}}}],[\"data\",{\"_index\":5,\"name\":{\"6\":{}},\"parent\":{\"37\":{},\"43\":{}}}],[\"data.enums\",{\"_index\":38,\"name\":{},\"parent\":{\"38\":{},\"40\":{}}}],[\"data.enums.role\",{\"_index\":42,\"name\":{},\"parent\":{\"41\":{},\"42\":{}}}],[\"data.enums.storagekey\",{\"_index\":40,\"name\":{},\"parent\":{\"39\":{}}}],[\"data.interfaces\",{\"_index\":46,\"name\":{},\"parent\":{\"44\":{}}}],[\"data.interfaces.user\",{\"_index\":48,\"name\":{},\"parent\":{\"45\":{},\"46\":{},\"47\":{},\"48\":{}}}],[\"default\",{\"_index\":1,\"name\":{\"1\":{},\"3\":{}},\"parent\":{}}],[\"editions\",{\"_index\":33,\"name\":{\"34\":{},\"48\":{}},\"parent\":{}}],[\"email\",{\"_index\":15,\"name\":{\"15\":{},\"20\":{}},\"parent\":{}}],[\"enums\",{\"_index\":36,\"name\":{\"37\":{}},\"parent\":{}}],[\"errors\",{\"_index\":56,\"name\":{\"55\":{}},\"parent\":{}}],[\"footer\",{\"_index\":9,\"name\":{\"10\":{}},\"parent\":{}}],[\"forbiddenpage\",{\"_index\":57,\"name\":{\"56\":{}},\"parent\":{}}],[\"gettoken\",{\"_index\":54,\"name\":{\"53\":{}},\"parent\":{}}],[\"infotext\",{\"_index\":23,\"name\":{\"25\":{}},\"parent\":{}}],[\"interfaces\",{\"_index\":44,\"name\":{\"43\":{}},\"parent\":{}}],[\"isloggedin\",{\"_index\":26,\"name\":{\"28\":{}},\"parent\":{}}],[\"localstorage\",{\"_index\":53,\"name\":{\"52\":{}},\"parent\":{}}],[\"logincomponents\",{\"_index\":10,\"name\":{\"11\":{}},\"parent\":{}}],[\"loginpage\",{\"_index\":60,\"name\":{\"58\":{}},\"parent\":{}}],[\"name\",{\"_index\":21,\"name\":{\"21\":{},\"46\":{}},\"parent\":{}}],[\"navbar\",{\"_index\":16,\"name\":{\"16\":{}},\"parent\":{}}],[\"notfoundpage\",{\"_index\":59,\"name\":{\"57\":{}},\"parent\":{}}],[\"osocletters\",{\"_index\":17,\"name\":{\"17\":{}},\"parent\":{}}],[\"password\",{\"_index\":14,\"name\":{\"14\":{},\"22\":{}},\"parent\":{}}],[\"pendingpage\",{\"_index\":61,\"name\":{\"59\":{}},\"parent\":{}}],[\"privateroute\",{\"_index\":18,\"name\":{\"18\":{}},\"parent\":{}}],[\"projectspage\",{\"_index\":62,\"name\":{\"60\":{}},\"parent\":{}}],[\"registercomponents\",{\"_index\":19,\"name\":{\"19\":{}},\"parent\":{}}],[\"registerpage\",{\"_index\":63,\"name\":{\"61\":{}},\"parent\":{}}],[\"role\",{\"_index\":29,\"name\":{\"30\":{},\"40\":{}},\"parent\":{}}],[\"router\",{\"_index\":2,\"name\":{\"2\":{}},\"parent\":{\"3\":{}}}],[\"setbearertoken\",{\"_index\":52,\"name\":{\"51\":{}},\"parent\":{}}],[\"seteditions\",{\"_index\":34,\"name\":{\"35\":{}},\"parent\":{}}],[\"setisloggedin\",{\"_index\":28,\"name\":{\"29\":{}},\"parent\":{}}],[\"setrole\",{\"_index\":30,\"name\":{\"31\":{}},\"parent\":{}}],[\"settoken\",{\"_index\":32,\"name\":{\"33\":{},\"54\":{}},\"parent\":{}}],[\"socialbuttons\",{\"_index\":13,\"name\":{\"13\":{},\"24\":{}},\"parent\":{}}],[\"storagekey\",{\"_index\":37,\"name\":{\"38\":{}},\"parent\":{}}],[\"studentspage\",{\"_index\":64,\"name\":{\"62\":{}},\"parent\":{}}],[\"token\",{\"_index\":31,\"name\":{\"32\":{}},\"parent\":{}}],[\"user\",{\"_index\":45,\"name\":{\"44\":{}},\"parent\":{}}],[\"userid\",{\"_index\":47,\"name\":{\"45\":{}},\"parent\":{}}],[\"userspage\",{\"_index\":65,\"name\":{\"63\":{}},\"parent\":{}}],[\"utils\",{\"_index\":6,\"name\":{\"7\":{}},\"parent\":{\"49\":{},\"52\":{}}}],[\"utils.api\",{\"_index\":51,\"name\":{},\"parent\":{\"50\":{},\"51\":{}}}],[\"utils.localstorage\",{\"_index\":55,\"name\":{},\"parent\":{\"53\":{},\"54\":{}}}],[\"validateregistrationurl\",{\"_index\":50,\"name\":{\"50\":{}},\"parent\":{}}],[\"verifyingtokenpage\",{\"_index\":66,\"name\":{\"64\":{}},\"parent\":{}}],[\"views\",{\"_index\":7,\"name\":{\"8\":{}},\"parent\":{\"55\":{},\"58\":{},\"59\":{},\"60\":{},\"61\":{},\"62\":{},\"63\":{},\"64\":{}}}],[\"views.errors\",{\"_index\":58,\"name\":{},\"parent\":{\"56\":{},\"57\":{}}}],[\"welcometext\",{\"_index\":11,\"name\":{\"12\":{}},\"parent\":{}}]],\"pipeline\":[]}}"); \ No newline at end of file +window.searchData = JSON.parse("{\"kinds\":{\"2\":\"Module\",\"4\":\"Namespace\",\"8\":\"Enumeration\",\"16\":\"Enumeration member\",\"64\":\"Function\",\"256\":\"Interface\",\"1024\":\"Property\",\"2048\":\"Method\"},\"rows\":[{\"id\":0,\"kind\":2,\"name\":\"App\",\"url\":\"modules/App.html\",\"classes\":\"tsd-kind-module\"},{\"id\":1,\"kind\":64,\"name\":\"default\",\"url\":\"modules/App.html#default\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"App\"},{\"id\":2,\"kind\":2,\"name\":\"Router\",\"url\":\"modules/Router.html\",\"classes\":\"tsd-kind-module\"},{\"id\":3,\"kind\":64,\"name\":\"default\",\"url\":\"modules/Router.html#default\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"Router\"},{\"id\":4,\"kind\":2,\"name\":\"components\",\"url\":\"modules/components.html\",\"classes\":\"tsd-kind-module\"},{\"id\":5,\"kind\":2,\"name\":\"contexts\",\"url\":\"modules/contexts.html\",\"classes\":\"tsd-kind-module\"},{\"id\":6,\"kind\":2,\"name\":\"data\",\"url\":\"modules/data.html\",\"classes\":\"tsd-kind-module\"},{\"id\":7,\"kind\":2,\"name\":\"utils\",\"url\":\"modules/utils.html\",\"classes\":\"tsd-kind-module\"},{\"id\":8,\"kind\":2,\"name\":\"views\",\"url\":\"modules/views.html\",\"classes\":\"tsd-kind-module\"},{\"id\":9,\"kind\":64,\"name\":\"AdminRoute\",\"url\":\"modules/components.html#AdminRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":10,\"kind\":64,\"name\":\"Footer\",\"url\":\"modules/components.html#Footer\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":11,\"kind\":4,\"name\":\"LoginComponents\",\"url\":\"modules/components.LoginComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":12,\"kind\":64,\"name\":\"WelcomeText\",\"url\":\"modules/components.LoginComponents.html#WelcomeText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":13,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/components.LoginComponents.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":14,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/components.LoginComponents.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":15,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/components.LoginComponents.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":16,\"kind\":64,\"name\":\"NavBar\",\"url\":\"modules/components.html#NavBar\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":17,\"kind\":64,\"name\":\"OSOCLetters\",\"url\":\"modules/components.html#OSOCLetters\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":18,\"kind\":64,\"name\":\"PrivateRoute\",\"url\":\"modules/components.html#PrivateRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":19,\"kind\":4,\"name\":\"RegisterComponents\",\"url\":\"modules/components.RegisterComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":20,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/components.RegisterComponents.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":21,\"kind\":64,\"name\":\"Name\",\"url\":\"modules/components.RegisterComponents.html#Name\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":22,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/components.RegisterComponents.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":23,\"kind\":64,\"name\":\"ConfirmPassword\",\"url\":\"modules/components.RegisterComponents.html#ConfirmPassword\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":24,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/components.RegisterComponents.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":25,\"kind\":64,\"name\":\"InfoText\",\"url\":\"modules/components.RegisterComponents.html#InfoText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":26,\"kind\":64,\"name\":\"BadInviteLink\",\"url\":\"modules/components.RegisterComponents.html#BadInviteLink\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":27,\"kind\":4,\"name\":\"UsersComponents\",\"url\":\"modules/components.UsersComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":28,\"kind\":4,\"name\":\"Coaches\",\"url\":\"modules/components.UsersComponents.Coaches.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents\"},{\"id\":29,\"kind\":64,\"name\":\"Coaches\",\"url\":\"modules/components.UsersComponents.Coaches.html#Coaches\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches\"},{\"id\":30,\"kind\":4,\"name\":\"CoachesComponents\",\"url\":\"modules/components.UsersComponents.Coaches.CoachesComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches\"},{\"id\":31,\"kind\":64,\"name\":\"AddCoach\",\"url\":\"modules/components.UsersComponents.Coaches.CoachesComponents.html#AddCoach\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches.CoachesComponents\"},{\"id\":32,\"kind\":64,\"name\":\"CoachItem\",\"url\":\"modules/components.UsersComponents.Coaches.CoachesComponents.html#CoachItem\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches.CoachesComponents\"},{\"id\":33,\"kind\":64,\"name\":\"CoachList\",\"url\":\"modules/components.UsersComponents.Coaches.CoachesComponents.html#CoachList\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches.CoachesComponents\"},{\"id\":34,\"kind\":64,\"name\":\"RemoveCoach\",\"url\":\"modules/components.UsersComponents.Coaches.CoachesComponents.html#RemoveCoach\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches.CoachesComponents\"},{\"id\":35,\"kind\":4,\"name\":\"InviteUser\",\"url\":\"modules/components.UsersComponents.InviteUser.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents\"},{\"id\":36,\"kind\":64,\"name\":\"InviteUser\",\"url\":\"modules/components.UsersComponents.InviteUser.html#InviteUser\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.InviteUser\"},{\"id\":37,\"kind\":4,\"name\":\"InviteUserComponents\",\"url\":\"modules/components.UsersComponents.InviteUser.InviteUserComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.InviteUser\"},{\"id\":38,\"kind\":64,\"name\":\"ButtonsDiv\",\"url\":\"modules/components.UsersComponents.InviteUser.InviteUserComponents.html#ButtonsDiv\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.InviteUser.InviteUserComponents\"},{\"id\":39,\"kind\":64,\"name\":\"ErrorDiv\",\"url\":\"modules/components.UsersComponents.InviteUser.InviteUserComponents.html#ErrorDiv\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.InviteUser.InviteUserComponents\"},{\"id\":40,\"kind\":64,\"name\":\"LinkDiv\",\"url\":\"modules/components.UsersComponents.InviteUser.InviteUserComponents.html#LinkDiv\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.InviteUser.InviteUserComponents\"},{\"id\":41,\"kind\":4,\"name\":\"PendingRequests\",\"url\":\"modules/components.UsersComponents.PendingRequests.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents\"},{\"id\":42,\"kind\":64,\"name\":\"PendingRequests\",\"url\":\"modules/components.UsersComponents.PendingRequests.html#PendingRequests\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests\"},{\"id\":43,\"kind\":4,\"name\":\"PendingRequestsComponents\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests\"},{\"id\":44,\"kind\":64,\"name\":\"AcceptReject\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html#AcceptReject\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests.PendingRequestsComponents\"},{\"id\":45,\"kind\":64,\"name\":\"RequestFilter\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html#RequestFilter\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests.PendingRequestsComponents\"},{\"id\":46,\"kind\":64,\"name\":\"RequestListItem\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html#RequestListItem\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests.PendingRequestsComponents\"},{\"id\":47,\"kind\":64,\"name\":\"RequestList\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html#RequestList\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests.PendingRequestsComponents\"},{\"id\":48,\"kind\":64,\"name\":\"RequestsHeader\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html#RequestsHeader\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests.PendingRequestsComponents\"},{\"id\":49,\"kind\":256,\"name\":\"AuthContextState\",\"url\":\"interfaces/contexts.AuthContextState.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-module\",\"parent\":\"contexts\"},{\"id\":50,\"kind\":1024,\"name\":\"isLoggedIn\",\"url\":\"interfaces/contexts.AuthContextState.html#isLoggedIn\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":51,\"kind\":2048,\"name\":\"setIsLoggedIn\",\"url\":\"interfaces/contexts.AuthContextState.html#setIsLoggedIn\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":52,\"kind\":1024,\"name\":\"role\",\"url\":\"interfaces/contexts.AuthContextState.html#role\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":53,\"kind\":2048,\"name\":\"setRole\",\"url\":\"interfaces/contexts.AuthContextState.html#setRole\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":54,\"kind\":1024,\"name\":\"token\",\"url\":\"interfaces/contexts.AuthContextState.html#token\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":55,\"kind\":2048,\"name\":\"setToken\",\"url\":\"interfaces/contexts.AuthContextState.html#setToken\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":56,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/contexts.AuthContextState.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":57,\"kind\":2048,\"name\":\"setEditions\",\"url\":\"interfaces/contexts.AuthContextState.html#setEditions\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":58,\"kind\":64,\"name\":\"AuthProvider\",\"url\":\"modules/contexts.html#AuthProvider\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"contexts\"},{\"id\":59,\"kind\":4,\"name\":\"Enums\",\"url\":\"modules/data.Enums.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"data\"},{\"id\":60,\"kind\":8,\"name\":\"StorageKey\",\"url\":\"enums/data.Enums.StorageKey.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"data.Enums\"},{\"id\":61,\"kind\":16,\"name\":\"BEARER_TOKEN\",\"url\":\"enums/data.Enums.StorageKey.html#BEARER_TOKEN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.StorageKey\"},{\"id\":62,\"kind\":8,\"name\":\"Role\",\"url\":\"enums/data.Enums.Role.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"data.Enums\"},{\"id\":63,\"kind\":16,\"name\":\"ADMIN\",\"url\":\"enums/data.Enums.Role.html#ADMIN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.Role\"},{\"id\":64,\"kind\":16,\"name\":\"COACH\",\"url\":\"enums/data.Enums.Role.html#COACH\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.Role\"},{\"id\":65,\"kind\":4,\"name\":\"Interfaces\",\"url\":\"modules/data.Interfaces.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"data\"},{\"id\":66,\"kind\":256,\"name\":\"User\",\"url\":\"interfaces/data.Interfaces.User.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"data.Interfaces\"},{\"id\":67,\"kind\":1024,\"name\":\"userId\",\"url\":\"interfaces/data.Interfaces.User.html#userId\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":68,\"kind\":1024,\"name\":\"name\",\"url\":\"interfaces/data.Interfaces.User.html#name\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":69,\"kind\":1024,\"name\":\"admin\",\"url\":\"interfaces/data.Interfaces.User.html#admin\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":70,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/data.Interfaces.User.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":71,\"kind\":4,\"name\":\"Api\",\"url\":\"modules/utils.Api.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"utils\"},{\"id\":72,\"kind\":64,\"name\":\"validateRegistrationUrl\",\"url\":\"modules/utils.Api.html#validateRegistrationUrl\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api\"},{\"id\":73,\"kind\":64,\"name\":\"setBearerToken\",\"url\":\"modules/utils.Api.html#setBearerToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api\"},{\"id\":74,\"kind\":4,\"name\":\"Users\",\"url\":\"modules/utils.Api.Users.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"utils.Api\"},{\"id\":75,\"kind\":4,\"name\":\"Admins\",\"url\":\"modules/utils.Api.Users.Admins.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users\"},{\"id\":76,\"kind\":64,\"name\":\"getAdmins\",\"url\":\"modules/utils.Api.Users.Admins.html#getAdmins\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Admins\"},{\"id\":77,\"kind\":64,\"name\":\"addAdmin\",\"url\":\"modules/utils.Api.Users.Admins.html#addAdmin\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Admins\"},{\"id\":78,\"kind\":64,\"name\":\"removeAdmin\",\"url\":\"modules/utils.Api.Users.Admins.html#removeAdmin\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Admins\"},{\"id\":79,\"kind\":64,\"name\":\"removeAdminAndCoach\",\"url\":\"modules/utils.Api.Users.Admins.html#removeAdminAndCoach\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Admins\"},{\"id\":80,\"kind\":4,\"name\":\"Coaches\",\"url\":\"modules/utils.Api.Users.Coaches.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users\"},{\"id\":81,\"kind\":64,\"name\":\"getCoaches\",\"url\":\"modules/utils.Api.Users.Coaches.html#getCoaches\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Coaches\"},{\"id\":82,\"kind\":64,\"name\":\"removeCoachFromEdition\",\"url\":\"modules/utils.Api.Users.Coaches.html#removeCoachFromEdition\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Coaches\"},{\"id\":83,\"kind\":64,\"name\":\"removeCoachFromAllEditions\",\"url\":\"modules/utils.Api.Users.Coaches.html#removeCoachFromAllEditions\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Coaches\"},{\"id\":84,\"kind\":64,\"name\":\"addCoachToEdition\",\"url\":\"modules/utils.Api.Users.Coaches.html#addCoachToEdition\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Coaches\"},{\"id\":85,\"kind\":4,\"name\":\"Requests\",\"url\":\"modules/utils.Api.Users.Requests.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users\"},{\"id\":86,\"kind\":64,\"name\":\"getRequests\",\"url\":\"modules/utils.Api.Users.Requests.html#getRequests\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Requests\"},{\"id\":87,\"kind\":64,\"name\":\"acceptRequest\",\"url\":\"modules/utils.Api.Users.Requests.html#acceptRequest\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Requests\"},{\"id\":88,\"kind\":64,\"name\":\"rejectRequest\",\"url\":\"modules/utils.Api.Users.Requests.html#rejectRequest\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Requests\"},{\"id\":89,\"kind\":256,\"name\":\"Request\",\"url\":\"interfaces/utils.Api.Users.Requests.Request.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Requests\"},{\"id\":90,\"kind\":1024,\"name\":\"requestId\",\"url\":\"interfaces/utils.Api.Users.Requests.Request.html#requestId\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Requests.Request\"},{\"id\":91,\"kind\":1024,\"name\":\"user\",\"url\":\"interfaces/utils.Api.Users.Requests.Request.html#user\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Requests.Request\"},{\"id\":92,\"kind\":256,\"name\":\"GetRequestsResponse\",\"url\":\"interfaces/utils.Api.Users.Requests.GetRequestsResponse.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Requests\"},{\"id\":93,\"kind\":1024,\"name\":\"requests\",\"url\":\"interfaces/utils.Api.Users.Requests.GetRequestsResponse.html#requests\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Requests.GetRequestsResponse\"},{\"id\":94,\"kind\":4,\"name\":\"Users\",\"url\":\"modules/utils.Api.Users.Users.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users\"},{\"id\":95,\"kind\":64,\"name\":\"getInviteLink\",\"url\":\"modules/utils.Api.Users.Users.html#getInviteLink\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Users\"},{\"id\":96,\"kind\":64,\"name\":\"getUsers\",\"url\":\"modules/utils.Api.Users.Users.html#getUsers\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Users\"},{\"id\":97,\"kind\":256,\"name\":\"User\",\"url\":\"interfaces/utils.Api.Users.Users.User.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Users\"},{\"id\":98,\"kind\":1024,\"name\":\"userId\",\"url\":\"interfaces/utils.Api.Users.Users.User.html#userId\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.User\"},{\"id\":99,\"kind\":1024,\"name\":\"name\",\"url\":\"interfaces/utils.Api.Users.Users.User.html#name\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.User\"},{\"id\":100,\"kind\":1024,\"name\":\"email\",\"url\":\"interfaces/utils.Api.Users.Users.User.html#email\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.User\"},{\"id\":101,\"kind\":1024,\"name\":\"admin\",\"url\":\"interfaces/utils.Api.Users.Users.User.html#admin\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.User\"},{\"id\":102,\"kind\":256,\"name\":\"UsersList\",\"url\":\"interfaces/utils.Api.Users.Users.UsersList.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Users\"},{\"id\":103,\"kind\":1024,\"name\":\"users\",\"url\":\"interfaces/utils.Api.Users.Users.UsersList.html#users\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.UsersList\"},{\"id\":104,\"kind\":256,\"name\":\"MailTo\",\"url\":\"interfaces/utils.Api.Users.Users.MailTo.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Users\"},{\"id\":105,\"kind\":1024,\"name\":\"mailTo\",\"url\":\"interfaces/utils.Api.Users.Users.MailTo.html#mailTo\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.MailTo\"},{\"id\":106,\"kind\":1024,\"name\":\"link\",\"url\":\"interfaces/utils.Api.Users.Users.MailTo.html#link\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.MailTo\"},{\"id\":107,\"kind\":4,\"name\":\"LocalStorage\",\"url\":\"modules/utils.LocalStorage.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"utils\"},{\"id\":108,\"kind\":64,\"name\":\"getToken\",\"url\":\"modules/utils.LocalStorage.html#getToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.LocalStorage\"},{\"id\":109,\"kind\":64,\"name\":\"setToken\",\"url\":\"modules/utils.LocalStorage.html#setToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.LocalStorage\"},{\"id\":110,\"kind\":4,\"name\":\"Errors\",\"url\":\"modules/views.Errors.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":111,\"kind\":64,\"name\":\"ForbiddenPage\",\"url\":\"modules/views.Errors.html#ForbiddenPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"views.Errors\"},{\"id\":112,\"kind\":64,\"name\":\"NotFoundPage\",\"url\":\"modules/views.Errors.html#NotFoundPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"views.Errors\"},{\"id\":113,\"kind\":64,\"name\":\"LoginPage\",\"url\":\"modules/views.html#LoginPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":114,\"kind\":64,\"name\":\"PendingPage\",\"url\":\"modules/views.html#PendingPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":115,\"kind\":64,\"name\":\"ProjectsPage\",\"url\":\"modules/views.html#ProjectsPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":116,\"kind\":64,\"name\":\"RegisterPage\",\"url\":\"modules/views.html#RegisterPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":117,\"kind\":64,\"name\":\"StudentsPage\",\"url\":\"modules/views.html#StudentsPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":118,\"kind\":64,\"name\":\"UsersPage\",\"url\":\"modules/views.html#UsersPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":119,\"kind\":64,\"name\":\"VerifyingTokenPage\",\"url\":\"modules/views.html#VerifyingTokenPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"parent\"],\"fieldVectors\":[[\"name/0\",[0,38.795]],[\"parent/0\",[]],[\"name/1\",[1,38.795]],[\"parent/1\",[0,3.784]],[\"name/2\",[2,38.795]],[\"parent/2\",[]],[\"name/3\",[1,38.795]],[\"parent/3\",[2,3.784]],[\"name/4\",[3,25.445]],[\"parent/4\",[]],[\"name/5\",[4,35.43]],[\"parent/5\",[]],[\"name/6\",[5,35.43]],[\"parent/6\",[]],[\"name/7\",[6,35.43]],[\"parent/7\",[]],[\"name/8\",[7,25.445]],[\"parent/8\",[]],[\"name/9\",[8,43.903]],[\"parent/9\",[3,2.482]],[\"name/10\",[9,43.903]],[\"parent/10\",[3,2.482]],[\"name/11\",[10,43.903]],[\"parent/11\",[3,2.482]],[\"name/12\",[11,43.903]],[\"parent/12\",[12,3.21]],[\"name/13\",[13,38.795]],[\"parent/13\",[12,3.21]],[\"name/14\",[14,38.795]],[\"parent/14\",[12,3.21]],[\"name/15\",[15,35.43]],[\"parent/15\",[12,3.21]],[\"name/16\",[16,43.903]],[\"parent/16\",[3,2.482]],[\"name/17\",[17,43.903]],[\"parent/17\",[3,2.482]],[\"name/18\",[18,43.903]],[\"parent/18\",[3,2.482]],[\"name/19\",[19,43.903]],[\"parent/19\",[3,2.482]],[\"name/20\",[15,35.43]],[\"parent/20\",[20,2.712]],[\"name/21\",[21,35.43]],[\"parent/21\",[20,2.712]],[\"name/22\",[14,38.795]],[\"parent/22\",[20,2.712]],[\"name/23\",[22,43.903]],[\"parent/23\",[20,2.712]],[\"name/24\",[13,38.795]],[\"parent/24\",[20,2.712]],[\"name/25\",[23,43.903]],[\"parent/25\",[20,2.712]],[\"name/26\",[24,43.903]],[\"parent/26\",[20,2.712]],[\"name/27\",[25,43.903]],[\"parent/27\",[3,2.482]],[\"name/28\",[26,35.43]],[\"parent/28\",[27,3.455]],[\"name/29\",[26,35.43]],[\"parent/29\",[28,3.784]],[\"name/30\",[29,43.903]],[\"parent/30\",[28,3.784]],[\"name/31\",[30,43.903]],[\"parent/31\",[31,3.21]],[\"name/32\",[32,43.903]],[\"parent/32\",[31,3.21]],[\"name/33\",[33,43.903]],[\"parent/33\",[31,3.21]],[\"name/34\",[34,43.903]],[\"parent/34\",[31,3.21]],[\"name/35\",[35,38.795]],[\"parent/35\",[27,3.455]],[\"name/36\",[35,38.795]],[\"parent/36\",[36,3.784]],[\"name/37\",[37,43.903]],[\"parent/37\",[36,3.784]],[\"name/38\",[38,43.903]],[\"parent/38\",[39,3.455]],[\"name/39\",[40,43.903]],[\"parent/39\",[39,3.455]],[\"name/40\",[41,43.903]],[\"parent/40\",[39,3.455]],[\"name/41\",[42,38.795]],[\"parent/41\",[27,3.455]],[\"name/42\",[42,38.795]],[\"parent/42\",[43,3.784]],[\"name/43\",[44,43.903]],[\"parent/43\",[43,3.784]],[\"name/44\",[45,43.903]],[\"parent/44\",[46,3.015]],[\"name/45\",[47,43.903]],[\"parent/45\",[46,3.015]],[\"name/46\",[48,43.903]],[\"parent/46\",[46,3.015]],[\"name/47\",[49,43.903]],[\"parent/47\",[46,3.015]],[\"name/48\",[50,43.903]],[\"parent/48\",[46,3.015]],[\"name/49\",[51,43.903]],[\"parent/49\",[4,3.455]],[\"name/50\",[52,43.903]],[\"parent/50\",[53,2.59]],[\"name/51\",[54,43.903]],[\"parent/51\",[53,2.59]],[\"name/52\",[55,38.795]],[\"parent/52\",[53,2.59]],[\"name/53\",[56,43.903]],[\"parent/53\",[53,2.59]],[\"name/54\",[57,43.903]],[\"parent/54\",[53,2.59]],[\"name/55\",[58,38.795]],[\"parent/55\",[53,2.59]],[\"name/56\",[59,38.795]],[\"parent/56\",[53,2.59]],[\"name/57\",[60,43.903]],[\"parent/57\",[53,2.59]],[\"name/58\",[61,43.903]],[\"parent/58\",[4,3.455]],[\"name/59\",[62,43.903]],[\"parent/59\",[5,3.455]],[\"name/60\",[63,43.903]],[\"parent/60\",[64,3.784]],[\"name/61\",[65,43.903]],[\"parent/61\",[66,4.282]],[\"name/62\",[55,38.795]],[\"parent/62\",[64,3.784]],[\"name/63\",[67,35.43]],[\"parent/63\",[68,3.784]],[\"name/64\",[69,43.903]],[\"parent/64\",[68,3.784]],[\"name/65\",[70,43.903]],[\"parent/65\",[5,3.455]],[\"name/66\",[71,35.43]],[\"parent/66\",[72,4.282]],[\"name/67\",[73,38.795]],[\"parent/67\",[74,3.21]],[\"name/68\",[21,35.43]],[\"parent/68\",[74,3.21]],[\"name/69\",[67,35.43]],[\"parent/69\",[74,3.21]],[\"name/70\",[59,38.795]],[\"parent/70\",[74,3.21]],[\"name/71\",[75,43.903]],[\"parent/71\",[6,3.455]],[\"name/72\",[76,43.903]],[\"parent/72\",[77,3.455]],[\"name/73\",[78,43.903]],[\"parent/73\",[77,3.455]],[\"name/74\",[79,35.43]],[\"parent/74\",[77,3.455]],[\"name/75\",[80,43.903]],[\"parent/75\",[81,3.21]],[\"name/76\",[82,43.903]],[\"parent/76\",[83,3.21]],[\"name/77\",[84,43.903]],[\"parent/77\",[83,3.21]],[\"name/78\",[85,43.903]],[\"parent/78\",[83,3.21]],[\"name/79\",[86,43.903]],[\"parent/79\",[83,3.21]],[\"name/80\",[26,35.43]],[\"parent/80\",[81,3.21]],[\"name/81\",[87,43.903]],[\"parent/81\",[88,3.21]],[\"name/82\",[89,43.903]],[\"parent/82\",[88,3.21]],[\"name/83\",[90,43.903]],[\"parent/83\",[88,3.21]],[\"name/84\",[91,43.903]],[\"parent/84\",[88,3.21]],[\"name/85\",[92,38.795]],[\"parent/85\",[81,3.21]],[\"name/86\",[93,43.903]],[\"parent/86\",[94,3.015]],[\"name/87\",[95,43.903]],[\"parent/87\",[94,3.015]],[\"name/88\",[96,43.903]],[\"parent/88\",[94,3.015]],[\"name/89\",[97,43.903]],[\"parent/89\",[94,3.015]],[\"name/90\",[98,43.903]],[\"parent/90\",[99,3.784]],[\"name/91\",[71,35.43]],[\"parent/91\",[99,3.784]],[\"name/92\",[100,43.903]],[\"parent/92\",[94,3.015]],[\"name/93\",[92,38.795]],[\"parent/93\",[101,4.282]],[\"name/94\",[79,35.43]],[\"parent/94\",[81,3.21]],[\"name/95\",[102,43.903]],[\"parent/95\",[103,3.015]],[\"name/96\",[104,43.903]],[\"parent/96\",[103,3.015]],[\"name/97\",[71,35.43]],[\"parent/97\",[103,3.015]],[\"name/98\",[73,38.795]],[\"parent/98\",[105,3.21]],[\"name/99\",[21,35.43]],[\"parent/99\",[105,3.21]],[\"name/100\",[15,35.43]],[\"parent/100\",[105,3.21]],[\"name/101\",[67,35.43]],[\"parent/101\",[105,3.21]],[\"name/102\",[106,43.903]],[\"parent/102\",[103,3.015]],[\"name/103\",[79,35.43]],[\"parent/103\",[107,4.282]],[\"name/104\",[108,38.795]],[\"parent/104\",[103,3.015]],[\"name/105\",[108,38.795]],[\"parent/105\",[109,3.784]],[\"name/106\",[110,43.903]],[\"parent/106\",[109,3.784]],[\"name/107\",[111,43.903]],[\"parent/107\",[6,3.455]],[\"name/108\",[112,43.903]],[\"parent/108\",[113,3.784]],[\"name/109\",[58,38.795]],[\"parent/109\",[113,3.784]],[\"name/110\",[114,43.903]],[\"parent/110\",[7,2.482]],[\"name/111\",[115,43.903]],[\"parent/111\",[116,3.784]],[\"name/112\",[117,43.903]],[\"parent/112\",[116,3.784]],[\"name/113\",[118,43.903]],[\"parent/113\",[7,2.482]],[\"name/114\",[119,43.903]],[\"parent/114\",[7,2.482]],[\"name/115\",[120,43.903]],[\"parent/115\",[7,2.482]],[\"name/116\",[121,43.903]],[\"parent/116\",[7,2.482]],[\"name/117\",[122,43.903]],[\"parent/117\",[7,2.482]],[\"name/118\",[123,43.903]],[\"parent/118\",[7,2.482]],[\"name/119\",[124,43.903]],[\"parent/119\",[7,2.482]]],\"invertedIndex\":[[\"acceptreject\",{\"_index\":45,\"name\":{\"44\":{}},\"parent\":{}}],[\"acceptrequest\",{\"_index\":95,\"name\":{\"87\":{}},\"parent\":{}}],[\"addadmin\",{\"_index\":84,\"name\":{\"77\":{}},\"parent\":{}}],[\"addcoach\",{\"_index\":30,\"name\":{\"31\":{}},\"parent\":{}}],[\"addcoachtoedition\",{\"_index\":91,\"name\":{\"84\":{}},\"parent\":{}}],[\"admin\",{\"_index\":67,\"name\":{\"63\":{},\"69\":{},\"101\":{}},\"parent\":{}}],[\"adminroute\",{\"_index\":8,\"name\":{\"9\":{}},\"parent\":{}}],[\"admins\",{\"_index\":80,\"name\":{\"75\":{}},\"parent\":{}}],[\"api\",{\"_index\":75,\"name\":{\"71\":{}},\"parent\":{}}],[\"app\",{\"_index\":0,\"name\":{\"0\":{}},\"parent\":{\"1\":{}}}],[\"authcontextstate\",{\"_index\":51,\"name\":{\"49\":{}},\"parent\":{}}],[\"authprovider\",{\"_index\":61,\"name\":{\"58\":{}},\"parent\":{}}],[\"badinvitelink\",{\"_index\":24,\"name\":{\"26\":{}},\"parent\":{}}],[\"bearer_token\",{\"_index\":65,\"name\":{\"61\":{}},\"parent\":{}}],[\"buttonsdiv\",{\"_index\":38,\"name\":{\"38\":{}},\"parent\":{}}],[\"coach\",{\"_index\":69,\"name\":{\"64\":{}},\"parent\":{}}],[\"coaches\",{\"_index\":26,\"name\":{\"28\":{},\"29\":{},\"80\":{}},\"parent\":{}}],[\"coachescomponents\",{\"_index\":29,\"name\":{\"30\":{}},\"parent\":{}}],[\"coachitem\",{\"_index\":32,\"name\":{\"32\":{}},\"parent\":{}}],[\"coachlist\",{\"_index\":33,\"name\":{\"33\":{}},\"parent\":{}}],[\"components\",{\"_index\":3,\"name\":{\"4\":{}},\"parent\":{\"9\":{},\"10\":{},\"11\":{},\"16\":{},\"17\":{},\"18\":{},\"19\":{},\"27\":{}}}],[\"components.logincomponents\",{\"_index\":12,\"name\":{},\"parent\":{\"12\":{},\"13\":{},\"14\":{},\"15\":{}}}],[\"components.registercomponents\",{\"_index\":20,\"name\":{},\"parent\":{\"20\":{},\"21\":{},\"22\":{},\"23\":{},\"24\":{},\"25\":{},\"26\":{}}}],[\"components.userscomponents\",{\"_index\":27,\"name\":{},\"parent\":{\"28\":{},\"35\":{},\"41\":{}}}],[\"components.userscomponents.coaches\",{\"_index\":28,\"name\":{},\"parent\":{\"29\":{},\"30\":{}}}],[\"components.userscomponents.coaches.coachescomponents\",{\"_index\":31,\"name\":{},\"parent\":{\"31\":{},\"32\":{},\"33\":{},\"34\":{}}}],[\"components.userscomponents.inviteuser\",{\"_index\":36,\"name\":{},\"parent\":{\"36\":{},\"37\":{}}}],[\"components.userscomponents.inviteuser.inviteusercomponents\",{\"_index\":39,\"name\":{},\"parent\":{\"38\":{},\"39\":{},\"40\":{}}}],[\"components.userscomponents.pendingrequests\",{\"_index\":43,\"name\":{},\"parent\":{\"42\":{},\"43\":{}}}],[\"components.userscomponents.pendingrequests.pendingrequestscomponents\",{\"_index\":46,\"name\":{},\"parent\":{\"44\":{},\"45\":{},\"46\":{},\"47\":{},\"48\":{}}}],[\"confirmpassword\",{\"_index\":22,\"name\":{\"23\":{}},\"parent\":{}}],[\"contexts\",{\"_index\":4,\"name\":{\"5\":{}},\"parent\":{\"49\":{},\"58\":{}}}],[\"contexts.authcontextstate\",{\"_index\":53,\"name\":{},\"parent\":{\"50\":{},\"51\":{},\"52\":{},\"53\":{},\"54\":{},\"55\":{},\"56\":{},\"57\":{}}}],[\"data\",{\"_index\":5,\"name\":{\"6\":{}},\"parent\":{\"59\":{},\"65\":{}}}],[\"data.enums\",{\"_index\":64,\"name\":{},\"parent\":{\"60\":{},\"62\":{}}}],[\"data.enums.role\",{\"_index\":68,\"name\":{},\"parent\":{\"63\":{},\"64\":{}}}],[\"data.enums.storagekey\",{\"_index\":66,\"name\":{},\"parent\":{\"61\":{}}}],[\"data.interfaces\",{\"_index\":72,\"name\":{},\"parent\":{\"66\":{}}}],[\"data.interfaces.user\",{\"_index\":74,\"name\":{},\"parent\":{\"67\":{},\"68\":{},\"69\":{},\"70\":{}}}],[\"default\",{\"_index\":1,\"name\":{\"1\":{},\"3\":{}},\"parent\":{}}],[\"editions\",{\"_index\":59,\"name\":{\"56\":{},\"70\":{}},\"parent\":{}}],[\"email\",{\"_index\":15,\"name\":{\"15\":{},\"20\":{},\"100\":{}},\"parent\":{}}],[\"enums\",{\"_index\":62,\"name\":{\"59\":{}},\"parent\":{}}],[\"errordiv\",{\"_index\":40,\"name\":{\"39\":{}},\"parent\":{}}],[\"errors\",{\"_index\":114,\"name\":{\"110\":{}},\"parent\":{}}],[\"footer\",{\"_index\":9,\"name\":{\"10\":{}},\"parent\":{}}],[\"forbiddenpage\",{\"_index\":115,\"name\":{\"111\":{}},\"parent\":{}}],[\"getadmins\",{\"_index\":82,\"name\":{\"76\":{}},\"parent\":{}}],[\"getcoaches\",{\"_index\":87,\"name\":{\"81\":{}},\"parent\":{}}],[\"getinvitelink\",{\"_index\":102,\"name\":{\"95\":{}},\"parent\":{}}],[\"getrequests\",{\"_index\":93,\"name\":{\"86\":{}},\"parent\":{}}],[\"getrequestsresponse\",{\"_index\":100,\"name\":{\"92\":{}},\"parent\":{}}],[\"gettoken\",{\"_index\":112,\"name\":{\"108\":{}},\"parent\":{}}],[\"getusers\",{\"_index\":104,\"name\":{\"96\":{}},\"parent\":{}}],[\"infotext\",{\"_index\":23,\"name\":{\"25\":{}},\"parent\":{}}],[\"interfaces\",{\"_index\":70,\"name\":{\"65\":{}},\"parent\":{}}],[\"inviteuser\",{\"_index\":35,\"name\":{\"35\":{},\"36\":{}},\"parent\":{}}],[\"inviteusercomponents\",{\"_index\":37,\"name\":{\"37\":{}},\"parent\":{}}],[\"isloggedin\",{\"_index\":52,\"name\":{\"50\":{}},\"parent\":{}}],[\"link\",{\"_index\":110,\"name\":{\"106\":{}},\"parent\":{}}],[\"linkdiv\",{\"_index\":41,\"name\":{\"40\":{}},\"parent\":{}}],[\"localstorage\",{\"_index\":111,\"name\":{\"107\":{}},\"parent\":{}}],[\"logincomponents\",{\"_index\":10,\"name\":{\"11\":{}},\"parent\":{}}],[\"loginpage\",{\"_index\":118,\"name\":{\"113\":{}},\"parent\":{}}],[\"mailto\",{\"_index\":108,\"name\":{\"104\":{},\"105\":{}},\"parent\":{}}],[\"name\",{\"_index\":21,\"name\":{\"21\":{},\"68\":{},\"99\":{}},\"parent\":{}}],[\"navbar\",{\"_index\":16,\"name\":{\"16\":{}},\"parent\":{}}],[\"notfoundpage\",{\"_index\":117,\"name\":{\"112\":{}},\"parent\":{}}],[\"osocletters\",{\"_index\":17,\"name\":{\"17\":{}},\"parent\":{}}],[\"password\",{\"_index\":14,\"name\":{\"14\":{},\"22\":{}},\"parent\":{}}],[\"pendingpage\",{\"_index\":119,\"name\":{\"114\":{}},\"parent\":{}}],[\"pendingrequests\",{\"_index\":42,\"name\":{\"41\":{},\"42\":{}},\"parent\":{}}],[\"pendingrequestscomponents\",{\"_index\":44,\"name\":{\"43\":{}},\"parent\":{}}],[\"privateroute\",{\"_index\":18,\"name\":{\"18\":{}},\"parent\":{}}],[\"projectspage\",{\"_index\":120,\"name\":{\"115\":{}},\"parent\":{}}],[\"registercomponents\",{\"_index\":19,\"name\":{\"19\":{}},\"parent\":{}}],[\"registerpage\",{\"_index\":121,\"name\":{\"116\":{}},\"parent\":{}}],[\"rejectrequest\",{\"_index\":96,\"name\":{\"88\":{}},\"parent\":{}}],[\"removeadmin\",{\"_index\":85,\"name\":{\"78\":{}},\"parent\":{}}],[\"removeadminandcoach\",{\"_index\":86,\"name\":{\"79\":{}},\"parent\":{}}],[\"removecoach\",{\"_index\":34,\"name\":{\"34\":{}},\"parent\":{}}],[\"removecoachfromalleditions\",{\"_index\":90,\"name\":{\"83\":{}},\"parent\":{}}],[\"removecoachfromedition\",{\"_index\":89,\"name\":{\"82\":{}},\"parent\":{}}],[\"request\",{\"_index\":97,\"name\":{\"89\":{}},\"parent\":{}}],[\"requestfilter\",{\"_index\":47,\"name\":{\"45\":{}},\"parent\":{}}],[\"requestid\",{\"_index\":98,\"name\":{\"90\":{}},\"parent\":{}}],[\"requestlist\",{\"_index\":49,\"name\":{\"47\":{}},\"parent\":{}}],[\"requestlistitem\",{\"_index\":48,\"name\":{\"46\":{}},\"parent\":{}}],[\"requests\",{\"_index\":92,\"name\":{\"85\":{},\"93\":{}},\"parent\":{}}],[\"requestsheader\",{\"_index\":50,\"name\":{\"48\":{}},\"parent\":{}}],[\"role\",{\"_index\":55,\"name\":{\"52\":{},\"62\":{}},\"parent\":{}}],[\"router\",{\"_index\":2,\"name\":{\"2\":{}},\"parent\":{\"3\":{}}}],[\"setbearertoken\",{\"_index\":78,\"name\":{\"73\":{}},\"parent\":{}}],[\"seteditions\",{\"_index\":60,\"name\":{\"57\":{}},\"parent\":{}}],[\"setisloggedin\",{\"_index\":54,\"name\":{\"51\":{}},\"parent\":{}}],[\"setrole\",{\"_index\":56,\"name\":{\"53\":{}},\"parent\":{}}],[\"settoken\",{\"_index\":58,\"name\":{\"55\":{},\"109\":{}},\"parent\":{}}],[\"socialbuttons\",{\"_index\":13,\"name\":{\"13\":{},\"24\":{}},\"parent\":{}}],[\"storagekey\",{\"_index\":63,\"name\":{\"60\":{}},\"parent\":{}}],[\"studentspage\",{\"_index\":122,\"name\":{\"117\":{}},\"parent\":{}}],[\"token\",{\"_index\":57,\"name\":{\"54\":{}},\"parent\":{}}],[\"user\",{\"_index\":71,\"name\":{\"66\":{},\"91\":{},\"97\":{}},\"parent\":{}}],[\"userid\",{\"_index\":73,\"name\":{\"67\":{},\"98\":{}},\"parent\":{}}],[\"users\",{\"_index\":79,\"name\":{\"74\":{},\"94\":{},\"103\":{}},\"parent\":{}}],[\"userscomponents\",{\"_index\":25,\"name\":{\"27\":{}},\"parent\":{}}],[\"userslist\",{\"_index\":106,\"name\":{\"102\":{}},\"parent\":{}}],[\"userspage\",{\"_index\":123,\"name\":{\"118\":{}},\"parent\":{}}],[\"utils\",{\"_index\":6,\"name\":{\"7\":{}},\"parent\":{\"71\":{},\"107\":{}}}],[\"utils.api\",{\"_index\":77,\"name\":{},\"parent\":{\"72\":{},\"73\":{},\"74\":{}}}],[\"utils.api.users\",{\"_index\":81,\"name\":{},\"parent\":{\"75\":{},\"80\":{},\"85\":{},\"94\":{}}}],[\"utils.api.users.admins\",{\"_index\":83,\"name\":{},\"parent\":{\"76\":{},\"77\":{},\"78\":{},\"79\":{}}}],[\"utils.api.users.coaches\",{\"_index\":88,\"name\":{},\"parent\":{\"81\":{},\"82\":{},\"83\":{},\"84\":{}}}],[\"utils.api.users.requests\",{\"_index\":94,\"name\":{},\"parent\":{\"86\":{},\"87\":{},\"88\":{},\"89\":{},\"92\":{}}}],[\"utils.api.users.requests.getrequestsresponse\",{\"_index\":101,\"name\":{},\"parent\":{\"93\":{}}}],[\"utils.api.users.requests.request\",{\"_index\":99,\"name\":{},\"parent\":{\"90\":{},\"91\":{}}}],[\"utils.api.users.users\",{\"_index\":103,\"name\":{},\"parent\":{\"95\":{},\"96\":{},\"97\":{},\"102\":{},\"104\":{}}}],[\"utils.api.users.users.mailto\",{\"_index\":109,\"name\":{},\"parent\":{\"105\":{},\"106\":{}}}],[\"utils.api.users.users.user\",{\"_index\":105,\"name\":{},\"parent\":{\"98\":{},\"99\":{},\"100\":{},\"101\":{}}}],[\"utils.api.users.users.userslist\",{\"_index\":107,\"name\":{},\"parent\":{\"103\":{}}}],[\"utils.localstorage\",{\"_index\":113,\"name\":{},\"parent\":{\"108\":{},\"109\":{}}}],[\"validateregistrationurl\",{\"_index\":76,\"name\":{\"72\":{}},\"parent\":{}}],[\"verifyingtokenpage\",{\"_index\":124,\"name\":{\"119\":{}},\"parent\":{}}],[\"views\",{\"_index\":7,\"name\":{\"8\":{}},\"parent\":{\"110\":{},\"113\":{},\"114\":{},\"115\":{},\"116\":{},\"117\":{},\"118\":{},\"119\":{}}}],[\"views.errors\",{\"_index\":116,\"name\":{},\"parent\":{\"111\":{},\"112\":{}}}],[\"welcometext\",{\"_index\":11,\"name\":{\"12\":{}},\"parent\":{}}]],\"pipeline\":[]}}"); \ No newline at end of file diff --git a/frontend/docs/enums/data.Enums.Role.html b/frontend/docs/enums/data.Enums.Role.html index 2c0e76c85..1c6b477bf 100644 --- a/frontend/docs/enums/data.Enums.Role.html +++ b/frontend/docs/enums/data.Enums.Role.html @@ -1,4 +1,4 @@ Role | OSOC 3 - Frontend Documentation
                    Options
                    All
                    • Public
                    • Public/Protected
                    • All
                    Menu

                    Enum for the different levels of authority a user can have

                    -

                    Index

                    Enumeration members

                    Enumeration members

                    ADMIN = 0
                    COACH = 1

                    Legend

                    • Namespace
                    • Function
                    • Interface

                    Settings

                    Theme

                    \ No newline at end of file +

                  Index

                  Enumeration members

                  Enumeration members

                  ADMIN = 0
                  COACH = 1

                  Legend

                  • Namespace
                  • Function
                  • Interface

                  Settings

                  Theme

                  \ No newline at end of file diff --git a/frontend/docs/enums/data.Enums.StorageKey.html b/frontend/docs/enums/data.Enums.StorageKey.html index a4449093e..b2773b327 100644 --- a/frontend/docs/enums/data.Enums.StorageKey.html +++ b/frontend/docs/enums/data.Enums.StorageKey.html @@ -1,5 +1,5 @@ StorageKey | OSOC 3 - Frontend Documentation
                  Options
                  All
                  • Public
                  • Public/Protected
                  • All
                  Menu

                  Enum for the keys in LocalStorage.

                  -

                  Index

                  Enumeration members

                  Enumeration members

                  BEARER_TOKEN = "bearerToken"
                  +

                  Index

                  Enumeration members

                  Enumeration members

                  BEARER_TOKEN = "bearerToken"

                  Bearer token used to authorize the user's requests in the backend.

                  Legend

                  • Namespace
                  • Function
                  • Interface

                  Settings

                  Theme

                  \ No newline at end of file diff --git a/frontend/docs/interfaces/contexts.AuthContextState.html b/frontend/docs/interfaces/contexts.AuthContextState.html index db218e95b..62d82b8be 100644 --- a/frontend/docs/interfaces/contexts.AuthContextState.html +++ b/frontend/docs/interfaces/contexts.AuthContextState.html @@ -1,3 +1,3 @@ AuthContextState | OSOC 3 - Frontend Documentation
                  Options
                  All
                  • Public
                  • Public/Protected
                  • All
                  Menu

                  Interface that holds the data stored in the AuthContext.

                  -

                  Hierarchy

                  • AuthContextState

                  Index

                  Properties

                  editions: number[]
                  isLoggedIn: null | boolean
                  role: null | Role
                  token: null | string

                  Methods

                  • setEditions(value: number[]): void
                  • setIsLoggedIn(value: null | boolean): void
                  • setRole(value: null | Role): void
                  • setToken(value: null | string): void

                  Legend

                  • Interface
                  • Property
                  • Method
                  • Namespace
                  • Function

                  Settings

                  Theme

                  \ No newline at end of file +

                Hierarchy

                • AuthContextState

                Index

                Properties

                editions: number[]
                isLoggedIn: null | boolean
                role: null | Role
                token: null | string

                Methods

                • setEditions(value: number[]): void
                • setIsLoggedIn(value: null | boolean): void
                • setRole(value: null | Role): void
                • setToken(value: null | string): void

                Legend

                • Interface
                • Property
                • Method
                • Namespace
                • Function

                Settings

                Theme

                \ No newline at end of file diff --git a/frontend/docs/interfaces/data.Interfaces.User.html b/frontend/docs/interfaces/data.Interfaces.User.html index 8671eeca9..bd5f08a50 100644 --- a/frontend/docs/interfaces/data.Interfaces.User.html +++ b/frontend/docs/interfaces/data.Interfaces.User.html @@ -2,4 +2,4 @@

                Data about a user using the application. Contains a list of edition names so that we can quickly check if they have access to a route or not.

                -

              Hierarchy

              • User

              Index

              Properties

              admin: boolean
              editions: string[]
              name: string
              userId: number

              Legend

              • Namespace
              • Function
              • Interface
              • Property

              Settings

              Theme

              \ No newline at end of file +

            Hierarchy

            • User

            Index

            Properties

            admin: boolean
            editions: string[]
            name: string
            userId: number

            Legend

            • Namespace
            • Function
            • Interface
            • Property

            Settings

            Theme

            \ No newline at end of file diff --git a/frontend/docs/modules/App.html b/frontend/docs/modules/App.html index c535e7b86..eb64b40b8 100644 --- a/frontend/docs/modules/App.html +++ b/frontend/docs/modules/App.html @@ -1,4 +1,4 @@ -App | OSOC 3 - Frontend Documentation
            Options
            All
            • Public
            • Public/Protected
            • All
            Menu

            Index

            Functions

            Functions

            • default(): Element
            • +App | OSOC 3 - Frontend Documentation
              Options
              All
              • Public
              • Public/Protected
              • All
              Menu

              Index

              Functions

              Functions

              • default(): Element

              Legend

              • Namespace
              • Function
              • Interface

              Settings

              Theme

              \ No newline at end of file diff --git a/frontend/docs/modules/Router.html b/frontend/docs/modules/Router.html index 1cf66e604..1f31876d9 100644 --- a/frontend/docs/modules/Router.html +++ b/frontend/docs/modules/Router.html @@ -1,4 +1,4 @@ -Router | OSOC 3 - Frontend Documentation
              Options
              All
              • Public
              • Public/Protected
              • All
              Menu

              Index

              Functions

              Functions

              • default(): Element
              • +Router | OSOC 3 - Frontend Documentation
                Options
                All
                • Public
                • Public/Protected
                • All
                Menu

                Index

                Functions

                Functions

                • default(): Element
                • Router component to render different pages depending on the current url. Renders the VerifyingTokenPage if the bearer token is still being validated.

                  Returns Element

                Legend

                • Namespace
                • Function
                • Interface

                Settings

                Theme

                \ No newline at end of file diff --git a/frontend/docs/modules/components.LoginComponents.html b/frontend/docs/modules/components.LoginComponents.html index b8b12739f..1d7aaab6b 100644 --- a/frontend/docs/modules/components.LoginComponents.html +++ b/frontend/docs/modules/components.LoginComponents.html @@ -1,9 +1,9 @@ -LoginComponents | OSOC 3 - Frontend Documentation
                Options
                All
                • Public
                • Public/Protected
                • All
                Menu

                Index

                Functions

                • Email(__namedParameters: { email: string; setEmail: any }): Element

                Legend

                • Namespace
                • Function
                • Interface

                Settings

                Theme

                \ No newline at end of file diff --git a/frontend/docs/modules/components.RegisterComponents.html b/frontend/docs/modules/components.RegisterComponents.html index 3524a2733..1c565b8bd 100644 --- a/frontend/docs/modules/components.RegisterComponents.html +++ b/frontend/docs/modules/components.RegisterComponents.html @@ -1,15 +1,15 @@ -RegisterComponents | OSOC 3 - Frontend Documentation
                Options
                All
                • Public
                • Public/Protected
                • All
                Menu

                Index

                Functions

                • BadInviteLink(): Element
                • SocialButtons(): Element

                Legend

                • Namespace
                • Function
                • Interface

                Settings

                Theme

                \ No newline at end of file diff --git a/frontend/docs/modules/components.html b/frontend/docs/modules/components.html index 26769a478..a9e07fec7 100644 --- a/frontend/docs/modules/components.html +++ b/frontend/docs/modules/components.html @@ -1,26 +1,26 @@ -components | OSOC 3 - Frontend Documentation
                Options
                All
                • Public
                • Public/Protected
                • All
                Menu

                Index

                Functions

                • AdminRoute(): Element

                Legend

                • Namespace
                • Function
                • Interface

                Settings

                Theme

                \ No newline at end of file diff --git a/frontend/docs/modules/contexts.html b/frontend/docs/modules/contexts.html index 312b17b06..10893ba07 100644 --- a/frontend/docs/modules/contexts.html +++ b/frontend/docs/modules/contexts.html @@ -1,4 +1,4 @@ -contexts | OSOC 3 - Frontend Documentation
                Options
                All
                • Public
                • Public/Protected
                • All
                Menu

                Index

                Interfaces

                Functions

                Functions

                • AuthProvider(__namedParameters: { children: ReactNode }): Element
                • +contexts | OSOC 3 - Frontend Documentation
                  Options
                  All
                  • Public
                  • Public/Protected
                  • All
                  Menu

                  Index

                  Interfaces

                  Functions

                  Functions

                  • AuthProvider(__namedParameters: { children: ReactNode }): Element
                  • Provider for auth that creates getters, setters, maintains state, and provides default values.

                    This keeps the main App component code clean by handling this diff --git a/frontend/docs/modules/utils.Api.html b/frontend/docs/modules/utils.Api.html index 374ff7c4d..0aacecc7d 100644 --- a/frontend/docs/modules/utils.Api.html +++ b/frontend/docs/modules/utils.Api.html @@ -1,7 +1,7 @@ -Api | OSOC 3 - Frontend Documentation

                    Options
                    All
                    • Public
                    • Public/Protected
                    • All
                    Menu

                    Index

                    Functions

                    • setBearerToken(value: null | string): void

                    Legend

                    • Namespace
                    • Function
                    • Interface

                    Settings

                    Theme

                    \ No newline at end of file diff --git a/frontend/docs/modules/utils.LocalStorage.html b/frontend/docs/modules/utils.LocalStorage.html index de2ce1baf..9d18be5b2 100644 --- a/frontend/docs/modules/utils.LocalStorage.html +++ b/frontend/docs/modules/utils.LocalStorage.html @@ -1,6 +1,6 @@ -LocalStorage | OSOC 3 - Frontend Documentation
                    Options
                    All
                    • Public
                    • Public/Protected
                    • All
                    Menu

                    Index

                    Functions

                    • getToken(): string | null
                    • +LocalStorage | OSOC 3 - Frontend Documentation
                      Options
                      All
                      • Public
                      • Public/Protected
                      • All
                      Menu

                      Index

                      Functions

                      • getToken(): string | null
                      • Function to pull the user's token out of LocalStorage. Returns null if there is no token in LocalStorage yet.

                        -

                        Returns string | null

                      • setToken(value: null | string): void
                      • setToken(value: null | string): void
                      • Function to set a new value for the bearer token in LocalStorage.

                        Parameters

                        • value: null | string

                        Returns void

                      Legend

                      • Namespace
                      • Function
                      • Interface

                      Settings

                      Theme

                      \ No newline at end of file diff --git a/frontend/docs/modules/views.Errors.html b/frontend/docs/modules/views.Errors.html index bdf65408e..667e202c5 100644 --- a/frontend/docs/modules/views.Errors.html +++ b/frontend/docs/modules/views.Errors.html @@ -1,7 +1,7 @@ -Errors | OSOC 3 - Frontend Documentation
                      Options
                      All
                      • Public
                      • Public/Protected
                      • All
                      Menu

                      Index

                      Functions

                      • ForbiddenPage(): Element
                      • +Errors | OSOC 3 - Frontend Documentation
                        Options
                        All
                        • Public
                        • Public/Protected
                        • All
                        Menu

                        Index

                        Functions

                        • ForbiddenPage(): Element
                        • Page shown to users when they try to access a resource they aren't authorized to. Examples include coaches performing admin actions, or coaches going to urls for editions they aren't part of.

                          -

                          Returns Element

                        • NotFoundPage(): Element
                        • NotFoundPage(): Element

                        Legend

                        • Namespace
                        • Function
                        • Interface

                        Settings

                        Theme

                        \ No newline at end of file diff --git a/frontend/docs/modules/views.html b/frontend/docs/modules/views.html index 4d61bcd40..e0ccf329b 100644 --- a/frontend/docs/modules/views.html +++ b/frontend/docs/modules/views.html @@ -1,12 +1,14 @@ -views | OSOC 3 - Frontend Documentation
                        Options
                        All
                        • Public
                        • Public/Protected
                        • All
                        Menu

                        Index

                        Functions

                        • LoginPage(): Element
                        • +views | OSOC 3 - Frontend Documentation
                          Options
                          All
                          • Public
                          • Public/Protected
                          • All
                          Menu

                          Index

                          Functions

                          • LoginPage(): Element
                          • PendingPage(): Element
                          • PendingPage(): Element
                          • ProjectsPage(): Element
                          • RegisterPage(): Element
                          • ProjectsPage(): Element
                          • RegisterPage(): Element
                          • StudentsPage(): Element
                          • UsersPage(): Element
                          • VerifyingTokenPage(): Element
                          • StudentsPage(): Element
                          • UsersPage(): Element
                          • VerifyingTokenPage(): Element

                          Legend

                          • Namespace
                          • Function
                          • Interface

                          Settings

                          Theme

                          \ No newline at end of file diff --git a/frontend/src/components/UsersComponents/Coaches/Coaches.tsx b/frontend/src/components/UsersComponents/Coaches/Coaches.tsx new file mode 100644 index 000000000..a7c17f082 --- /dev/null +++ b/frontend/src/components/UsersComponents/Coaches/Coaches.tsx @@ -0,0 +1,79 @@ +import React, { useEffect, useState } from "react"; +import { CoachesTitle, CoachesContainer } from "./styles"; +import { getUsers, User } from "../../../utils/api/users/users"; +import { Error, SearchInput } from "../PendingRequests/styles"; +import { getCoaches } from "../../../utils/api/users/coaches"; +import { CoachList, AddCoach } from "./CoachesComponents"; + +/** + * + * @param props + * @constructor + */ +export default function Coaches(props: { edition: string }) { + const [allCoaches, setAllCoaches] = useState([]); + const [coaches, setCoaches] = useState([]); + const [users, setUsers] = useState([]); + const [gettingData, setGettingData] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const [gotData, setGotData] = useState(false); + const [error, setError] = useState(""); + + async function getData() { + setGettingData(true); + setGotData(false); + try { + const coachResponse = await getCoaches(props.edition); + setAllCoaches(coachResponse.users); + setCoaches(coachResponse.users); + + const UsersResponse = await getUsers(); + const users = []; + for (const user of UsersResponse.users) { + if (!coachResponse.users.some(e => e.userId === user.userId)) { + users.push(user); + } + } + setUsers(users); + + setGotData(true); + setGettingData(false); + } catch (exception) { + setError("Oops, something went wrong..."); + setGettingData(false); + } + } + + useEffect(() => { + if (!gotData && !gettingData && !error) { + getData(); + } + }, [gotData, gettingData, error, getData]); + + const filter = (word: string) => { + setSearchTerm(word); + const newCoaches: User[] = []; + for (const coach of allCoaches) { + if (coach.name.toUpperCase().includes(word.toUpperCase())) { + newCoaches.push(coach); + } + } + setCoaches(newCoaches); + }; + + return ( + + Coaches + filter(e.target.value)} /> + + + {error} + + ); +} diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx new file mode 100644 index 000000000..c3aa6c801 --- /dev/null +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx @@ -0,0 +1,79 @@ +import { User } from "../../../../utils/api/users/users"; +import React, { useState } from "react"; +import { addCoachToEdition } from "../../../../utils/api/users/coaches"; +import { AddAdminButton, ModalContentGreen } from "../../../../views/AdminsPage/Admins/styles"; +import { Button, Modal } from "react-bootstrap"; +import { Typeahead } from "react-bootstrap-typeahead"; +import { Error } from "../../PendingRequests/styles"; + +export default function AddCoach(props: { users: User[]; edition: string; refresh: () => void }) { + const [show, setShow] = useState(false); + const [selected, setSelected] = useState(undefined); + const [error, setError] = useState(""); + + const handleClose = () => { + setSelected(undefined); + setShow(false); + }; + const handleShow = () => setShow(true); + + async function addCoach(userId: number) { + try { + const added = await addCoachToEdition(userId, props.edition); + if (added) { + props.refresh(); + handleClose(); + } else { + setError("Something went wrong. Failed to add coach"); + } + } catch (error) { + setError("Something went wrong. Failed to add coach"); + } + } + + return ( + <> + + Add coach + + + + + + Add Coach + + + { + setSelected(selected[0] as User); + }} + id="non-coach-users" + options={props.users} + labelKey="name" + filterBy={["name"]} + emptyLabel="No users found." + placeholder={"user's name"} + /> + + + + + {error} + + + + + ); +} diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx new file mode 100644 index 000000000..80ed5adf0 --- /dev/null +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx @@ -0,0 +1,54 @@ +import { User } from "../../../../utils/api/users/users"; +import { SpinnerContainer } from "../../PendingRequests/styles"; +import { Spinner } from "react-bootstrap"; +import { CoachesTable } from "../styles"; +import React from "react"; +import { CoachItem } from "./index"; + +export default function CoachList(props: { + coaches: User[]; + loading: boolean; + edition: string; + gotData: boolean; + refresh: () => void; +}) { + if (props.loading) { + return ( + + + + ); + } else if (props.coaches.length === 0) { + if (props.gotData) { + return
                          No coaches for this edition
                          ; + } else { + return null; + } + } + + const body = ( + + {props.coaches.map(coach => ( + + ))} + + ); + + return ( + + + + Name + Email + Remove from edition + + + {body} + + ); +} diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx new file mode 100644 index 000000000..2b6423887 --- /dev/null +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx @@ -0,0 +1,19 @@ +import { User } from "../../../../utils/api/users/users"; +import React from "react"; +import RemoveCoach from "./RemoveCoach"; + +export default function CoachListItem(props: { + coach: User; + edition: string; + refresh: () => void; +}) { + return ( + + {props.coach.name} + {props.coach.email} + + + + + ); +} diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx new file mode 100644 index 000000000..7c903b4d2 --- /dev/null +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx @@ -0,0 +1,82 @@ +import { User } from "../../../../utils/api/users/users"; +import React, { useState } from "react"; +import { + removeCoachFromAllEditions, + removeCoachFromEdition, +} from "../../../../utils/api/users/coaches"; +import { Button, Modal } from "react-bootstrap"; +import { ModalContent } from "../styles"; +import { Error } from "../../PendingRequests/styles"; + +export default function RemoveCoach(props: { coach: User; edition: string; refresh: () => void }) { + const [show, setShow] = useState(false); + const [error, setError] = useState(""); + + const handleClose = () => setShow(false); + const handleShow = () => { + setShow(true); + setError(""); + }; + + async function removeCoach(userId: number, allEditions: boolean) { + try { + let removed; + if (allEditions) { + removed = await removeCoachFromAllEditions(userId); + } else { + removed = await removeCoachFromEdition(userId, props.edition); + } + + if (removed) { + props.refresh(); + handleClose(); + } else { + setError("Something went wrong. Failed to remove coach"); + } + } catch (error) { + setError("Something went wrong. Failed to remove coach"); + } + } + + return ( + <> + + + + + + Remove Coach + + +

                          {props.coach.name}

                          + {props.coach.email} +
                          + + + + + {error} + +
                          +
                          + + ); +} diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/index.ts b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/index.ts new file mode 100644 index 000000000..342f63cd7 --- /dev/null +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/index.ts @@ -0,0 +1,4 @@ +export { default as AddCoach } from "./AddCoach"; +export { default as CoachItem } from "./CoachListItem"; +export { default as CoachList } from "./CoachList"; +export { default as RemoveCoach } from "./RemoveCoach"; diff --git a/frontend/src/components/UsersComponents/Coaches/index.ts b/frontend/src/components/UsersComponents/Coaches/index.ts new file mode 100644 index 000000000..881394497 --- /dev/null +++ b/frontend/src/components/UsersComponents/Coaches/index.ts @@ -0,0 +1,2 @@ +export { default as Coaches } from "./Coaches"; +export * as CoachesComponents from "./CoachesComponents"; diff --git a/frontend/src/views/UsersPage/Coaches/styles.ts b/frontend/src/components/UsersComponents/Coaches/styles.ts similarity index 100% rename from frontend/src/views/UsersPage/Coaches/styles.ts rename to frontend/src/components/UsersComponents/Coaches/styles.ts diff --git a/frontend/src/views/UsersPage/InviteUser/InviteUsers.css b/frontend/src/components/UsersComponents/InviteUser/InviteUser.css similarity index 100% rename from frontend/src/views/UsersPage/InviteUser/InviteUsers.css rename to frontend/src/components/UsersComponents/InviteUser/InviteUser.css diff --git a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx similarity index 53% rename from frontend/src/views/UsersPage/InviteUser/InviteUser.tsx rename to frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx index ced2c13f9..f8988ebb2 100644 --- a/frontend/src/views/UsersPage/InviteUser/InviteUser.tsx +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx @@ -1,33 +1,8 @@ import React, { useState } from "react"; import { getInviteLink } from "../../../utils/api/users/users"; -import "./InviteUsers.css"; -import { InviteInput, InviteButton, Loader, InviteContainer, Link, Error } from "./styles"; - -function ButtonDiv(props: { loading: boolean; onClick: () => void }) { - let buttonDiv; - if (props.loading) { - buttonDiv = ; - } else { - buttonDiv = Send invite; - } - return buttonDiv; -} - -function ErrorDiv(props: { errorMessage: string }) { - let errorDiv = null; - if (props.errorMessage) { - errorDiv = {props.errorMessage}; - } - return errorDiv; -} - -function LinkDiv(props: { link: string }) { - let linkDiv = null; - if (props.link) { - linkDiv = {props.link}; - } - return linkDiv; -} +import "./InviteUser.css"; +import { InviteInput, InviteContainer } from "./styles"; +import { ButtonsDiv, ErrorDiv, LinkDiv } from "./InviteUserComponents"; export default function InviteUser(props: { edition: string }) { const [email, setEmail] = useState(""); @@ -43,13 +18,22 @@ export default function InviteUser(props: { edition: string }) { setErrorMessage(""); }; - const sendInvite = async () => { + const sendInvite = async (copyInvite: boolean) => { if (/[^@\s]+@[^@\s]+\.[^@\s]+/.test(email)) { setLoading(true); - const ding = await getInviteLink(props.edition, email); - setLink(ding); - setLoading(false); - // TODO: fix email stuff + try { + const response = await getInviteLink(props.edition, email); + if (copyInvite) { + await navigator.clipboard.writeText(response.mailTo); + } else { + window.open(response.mailTo); + } + setLoading(false); + setEmail(""); + } catch (error) { + setLoading(false); + setErrorMessage("Something went wrong"); + } } else { setValid(false); setErrorMessage("Invalid email"); @@ -64,7 +48,7 @@ export default function InviteUser(props: { edition: string }) { value={email} onChange={e => changeEmail(e.target.value)} /> - + diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ButtonsDiv.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ButtonsDiv.tsx new file mode 100644 index 000000000..76b9458b6 --- /dev/null +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ButtonsDiv.tsx @@ -0,0 +1,17 @@ +import { CopyButton, InviteButton, Loader } from "../styles"; +import React from "react"; + +export default function ButtonsDiv(props: { + loading: boolean; + sendInvite: (copy: boolean) => void; +}) { + if (props.loading) { + return ; + } + return ( +
                          + props.sendInvite(false)}>Send invite + props.sendInvite(true)}>Copy invite +
                          + ); +} diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ErrorDiv.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ErrorDiv.tsx new file mode 100644 index 000000000..b58b35ccb --- /dev/null +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ErrorDiv.tsx @@ -0,0 +1,10 @@ +import { Error } from "../styles"; +import React from "react"; + +export default function ErrorDiv(props: { errorMessage: string }) { + let errorDiv = null; + if (props.errorMessage) { + errorDiv = {props.errorMessage}; + } + return errorDiv; +} diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/LinkDiv.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/LinkDiv.tsx new file mode 100644 index 000000000..8065ae6ad --- /dev/null +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/LinkDiv.tsx @@ -0,0 +1,10 @@ +import { Link } from "../styles"; +import React from "react"; + +export default function LinkDiv(props: { link: string }) { + let linkDiv = null; + if (props.link) { + linkDiv = {props.link}; + } + return linkDiv; +} diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/index.ts b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/index.ts new file mode 100644 index 000000000..8542f73b7 --- /dev/null +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/index.ts @@ -0,0 +1,3 @@ +export { default as ButtonsDiv } from "./ButtonsDiv"; +export { default as ErrorDiv } from "./ErrorDiv"; +export { default as LinkDiv } from "./LinkDiv"; diff --git a/frontend/src/components/UsersComponents/InviteUser/index.ts b/frontend/src/components/UsersComponents/InviteUser/index.ts new file mode 100644 index 000000000..32de38c24 --- /dev/null +++ b/frontend/src/components/UsersComponents/InviteUser/index.ts @@ -0,0 +1,2 @@ +export { default as InviteUser } from "./InviteUser"; +export * as InviteUserComponents from "./InviteUserComponents"; diff --git a/frontend/src/views/UsersPage/InviteUser/styles.ts b/frontend/src/components/UsersComponents/InviteUser/styles.ts similarity index 86% rename from frontend/src/views/UsersPage/InviteUser/styles.ts rename to frontend/src/components/UsersComponents/InviteUser/styles.ts index 9d9e8a06c..a48b94985 100644 --- a/frontend/src/views/UsersPage/InviteUser/styles.ts +++ b/frontend/src/components/UsersComponents/InviteUser/styles.ts @@ -30,6 +30,16 @@ export const InviteButton = styled(Button).attrs({ margin-top: 10px; `; +export const CopyButton = styled(Button).attrs({ + size: "sm", +})` + cursor: pointer; + background: var(--osoc_orange); + color: black; + margin-left: 7px; + margin-top: 10px; +`; + const rotate = keyframes` from { transform: rotate(0deg); diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx new file mode 100644 index 000000000..97eede932 --- /dev/null +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx @@ -0,0 +1,68 @@ +import React, { useEffect, useState } from "react"; +import Collapsible from "react-collapsible"; +import { PendingRequestsContainer, Error } from "./styles"; +import { getRequests, Request } from "../../../utils/api/users/requests"; +import { RequestFilter, RequestsHeader } from "./PendingRequestsComponents"; + +function RequestsList(props: { loading: boolean; gotData: boolean; requests: Request[] }) { + return null; +} + +export default function PendingRequests(props: { edition: string }) { + const [allRequests, setAllRequests] = useState([]); + const [requests, setRequests] = useState([]); + const [gettingRequests, setGettingRequests] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const [gotData, setGotData] = useState(false); + const [open, setOpen] = useState(false); + const [error, setError] = useState(""); + + async function getData() { + try { + const response = await getRequests(props.edition); + setAllRequests(response.requests); + setRequests(response.requests); + setGotData(true); + setGettingRequests(false); + } catch (exception) { + setError("Oops, something went wrong..."); + setGettingRequests(false); + } + } + + useEffect(() => { + if (!gotData && !gettingRequests && !error) { + setGettingRequests(true); + getData(); + } + }, [gotData, gettingRequests, error, getData]); + + const filter = (word: string) => { + setSearchTerm(word); + const newRequests: Request[] = []; + for (const request of allRequests) { + if (request.user.name.toUpperCase().includes(word.toUpperCase())) { + newRequests.push(request); + } + } + setRequests(newRequests); + }; + + return ( + + } + onOpening={() => setOpen(true)} + onClosing={() => setOpen(false)} + > + filter(word)} + show={allRequests.length > 0} + /> + + {error} + + + ); +} diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/AcceptReject.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/AcceptReject.tsx new file mode 100644 index 000000000..3149ff550 --- /dev/null +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/AcceptReject.tsx @@ -0,0 +1,12 @@ +import { AcceptButton, RejectButton } from "../styles"; +import { acceptRequest, rejectRequest } from "../../../../utils/api/users/requests"; +import React from "react"; + +export default function AcceptReject(props: { requestId: number }) { + return ( +
                          + acceptRequest(props.requestId)}>Accept + rejectRequest(props.requestId)}>Reject +
                          + ); +} diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestFilter.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestFilter.tsx new file mode 100644 index 000000000..59a555752 --- /dev/null +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestFilter.tsx @@ -0,0 +1,16 @@ +import { SearchInput } from "../styles"; +import React from "react"; + +export default function RequestFilter(props: { + searchTerm: string; + filter: (key: string) => void; + show: boolean; +}) { + if (props.show) { + return ( + props.filter(e.target.value)} /> + ); + } else { + return null; + } +} diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx new file mode 100644 index 000000000..9965484ba --- /dev/null +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx @@ -0,0 +1,46 @@ +import { Request } from "../../../../utils/api/users/requests"; +import { AcceptRejectTh, RequestsTable, SpinnerContainer } from "../styles"; +import { Spinner } from "react-bootstrap"; +import React from "react"; +import RequestListItem from "./RequestListItem"; + +export default function RequestList(props: { + requests: Request[]; + loading: boolean; + gotData: boolean; +}) { + if (props.loading) { + return ( + + + + ); + } else if (props.requests.length === 0) { + if (props.gotData) { + return
                          No requests
                          ; + } else { + return null; + } + } + + const body = ( + + {props.requests.map(request => ( + + ))} + + ); + + return ( + + + + Name + Email + Accept/Reject + + + {body} + + ); +} diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestListItem.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestListItem.tsx new file mode 100644 index 000000000..dd6bcb2c0 --- /dev/null +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestListItem.tsx @@ -0,0 +1,15 @@ +import { Request } from "../../../../utils/api/users/requests"; +import React from "react"; +import AcceptReject from "./AcceptReject"; + +export default function RequestListItem(props: { request: Request }) { + return ( + + {props.request.user.name} + {props.request.user.email} + + + + + ); +} diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestsHeader.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestsHeader.tsx new file mode 100644 index 000000000..e87e73e1f --- /dev/null +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestsHeader.tsx @@ -0,0 +1,19 @@ +import { ClosedArrow, OpenArrow, RequestHeaderDiv, RequestHeaderTitle } from "../styles"; +import React from "react"; + +function Arrow(props: { open: boolean }) { + if (props.open) { + return ; + } else { + return ; + } +} + +export default function RequestsHeader(props: { open: boolean }) { + return ( + + Requests + + + ); +} diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/index.ts b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/index.ts new file mode 100644 index 000000000..f4e0f0475 --- /dev/null +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/index.ts @@ -0,0 +1,5 @@ +export { default as AcceptReject } from "./AcceptReject"; +export { default as RequestFilter } from "./RequestFilter"; +export { default as RequestListItem } from "./RequestListItem"; +export { default as RequestList } from "./RequestList"; +export { default as RequestsHeader } from "./RequestsHeader"; diff --git a/frontend/src/components/UsersComponents/PendingRequests/index.ts b/frontend/src/components/UsersComponents/PendingRequests/index.ts new file mode 100644 index 000000000..ac5bdf1ca --- /dev/null +++ b/frontend/src/components/UsersComponents/PendingRequests/index.ts @@ -0,0 +1,2 @@ +export { default as PendingRequests } from "./PendingRequests"; +export * as PendingRequestsComponents from "./PendingRequestsComponents"; diff --git a/frontend/src/views/UsersPage/PendingRequests/styles.ts b/frontend/src/components/UsersComponents/PendingRequests/styles.ts similarity index 100% rename from frontend/src/views/UsersPage/PendingRequests/styles.ts rename to frontend/src/components/UsersComponents/PendingRequests/styles.ts diff --git a/frontend/src/components/UsersComponents/index.ts b/frontend/src/components/UsersComponents/index.ts new file mode 100644 index 000000000..bf6dda9d5 --- /dev/null +++ b/frontend/src/components/UsersComponents/index.ts @@ -0,0 +1,3 @@ +export * as Coaches from "./Coaches"; +export * as InviteUser from "./InviteUser"; +export * as PendingRequests from "./PendingRequests"; diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index ffe2ef414..b510a6011 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -5,3 +5,4 @@ export { default as NavBar } from "./navbar"; export { default as OSOCLetters } from "./OSOCLetters"; export { default as PrivateRoute } from "./PrivateRoute"; export * as RegisterComponents from "./RegisterComponents"; +export * as UsersComponents from "./UsersComponents"; diff --git a/frontend/src/utils/api/index.ts b/frontend/src/utils/api/index.ts index ef93639c0..f6672630f 100644 --- a/frontend/src/utils/api/index.ts +++ b/frontend/src/utils/api/index.ts @@ -1,2 +1,3 @@ export { validateRegistrationUrl } from "./auth"; export { setBearerToken } from "./api"; +export * as Users from "./users"; diff --git a/frontend/src/utils/api/users/admins.ts b/frontend/src/utils/api/users/admins.ts index 1d8167153..e7202af97 100644 --- a/frontend/src/utils/api/users/admins.ts +++ b/frontend/src/utils/api/users/admins.ts @@ -1,25 +1,36 @@ -import { User } from "./users"; +import { UsersList } from "./users"; import { axiosInstance } from "../api"; -export interface GetAdminsResponse { - users: User[]; -} - -export async function getAdmins(): Promise { +/** + * Get all admins + */ +export async function getAdmins(): Promise { const response = await axiosInstance.get(`/users?admin=true`); - return response.data as GetAdminsResponse; + return response.data as UsersList; } +/** + * 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; } +/** + * Remove the given user as admin + * @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; } +/** + * Remove the given user as admin and remove him as coach for every edition + * @param {number} userId The id of the user + */ export async function removeAdminAndCoach(userId: number) { const response = await axiosInstance.patch(`/users/${userId}`, { admin: false }); // TODO: remove user from all editions diff --git a/frontend/src/utils/api/users/coaches.ts b/frontend/src/utils/api/users/coaches.ts index 163337363..8f7e03a5f 100644 --- a/frontend/src/utils/api/users/coaches.ts +++ b/frontend/src/utils/api/users/coaches.ts @@ -1,25 +1,39 @@ -import { User } from "./users"; +import { UsersList } from "./users"; import { axiosInstance } from "../api"; -export interface GetCoachesResponse { - users: User[]; -} - -export async function getCoaches(edition: string): Promise { +/** + * Get all coaches from the given edition + * @param {string} edition The edition name + */ +export async function getCoaches(edition: string): Promise { const response = await axiosInstance.get(`/users/?edition=${edition}`); - return response.data as GetCoachesResponse; + return response.data as UsersList; } +/** + * Remove a user as coach from the given edition + * @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; } +/** + * Remove a user as coach from all editions + * @param {number} userId The user's id + */ export async function removeCoachFromAllEditions(userId: number): Promise { // TODO: sent correct DELETE return false; } +/** + * Add a user as coach to an edition + * @param {number} userId The user's id + * @param {string} edition The edition's name + */ export async function addCoachToEdition(userId: number, edition: string): Promise { const response = await axiosInstance.post(`/users/${userId}/editions/${edition}`); return response.status === 204; diff --git a/frontend/src/utils/api/users/index.ts b/frontend/src/utils/api/users/index.ts new file mode 100644 index 000000000..8e2828359 --- /dev/null +++ b/frontend/src/utils/api/users/index.ts @@ -0,0 +1,4 @@ +export * as Admins from "./admins"; +export * as Coaches from "./coaches"; +export * as Requests from "./requests"; +export * as Users from "./users"; diff --git a/frontend/src/utils/api/users/requests.ts b/frontend/src/utils/api/users/requests.ts index 103356139..055facbc5 100644 --- a/frontend/src/utils/api/users/requests.ts +++ b/frontend/src/utils/api/users/requests.ts @@ -1,24 +1,42 @@ import { User } from "./users"; import { axiosInstance } from "../api"; +/** + * Interface for a request + */ export interface Request { requestId: number; user: User; } +/** + * Interface for a list of requests + */ export interface GetRequestsResponse { requests: Request[]; } +/** + * Get all pending requests of a given edition + * @param {string} edition The edition's name + */ export async function getRequests(edition: string): Promise { const response = await axiosInstance.get(`/users/requests?edition=${edition}`); return response.data as GetRequestsResponse; } +/** + * Accept a coach request + * @param {number} requestId The id of the request + */ export async function acceptRequest(requestId: number) { alert("Accept " + requestId); } +/** + * Reject a coach request + * @param {number} requestId The id of the request + */ export async function rejectRequest(requestId: number) { alert("Reject " + requestId); } diff --git a/frontend/src/utils/api/users/users.ts b/frontend/src/utils/api/users/users.ts index 54dcb7fe7..736fb94e0 100644 --- a/frontend/src/utils/api/users/users.ts +++ b/frontend/src/utils/api/users/users.ts @@ -1,6 +1,8 @@ -import axios from "axios"; import { axiosInstance } from "../api"; +/** + * Interface for a user + */ export interface User { userId: number; name: string; @@ -8,31 +10,31 @@ export interface User { admin: boolean; } +export interface UsersList { + users: User[]; +} + /** - * Get invite link for given email and edition + * Interface for a mailto link */ -export async function getInviteLink(edition: string, email: string): Promise { - try { - await axiosInstance - .post(`/editions/${edition}/invites/`, { email: email }) - .then(response => { - return response.data.mailTo; - }); - } catch (error) { - if (axios.isAxiosError(error)) { - return error.message; - } else { - throw error; - } - } - return ""; +export interface MailTo { + mailTo: string; + link: string; } -export interface GetUsersResponse { - users: User[]; +/** + * Get invite link for given email and edition + */ +export async function getInviteLink(edition: string, email: string): Promise { + const response = await axiosInstance.post(`/editions/${edition}/invites/`, { email: email }); + console.log(response); + return response.data as MailTo; } -export async function getUsers(): Promise { +/** + * Get all users + */ +export async function getUsers(): Promise { const response = await axiosInstance.get(`/users`); - return response.data as GetUsersResponse; + return response.data as UsersList; } diff --git a/frontend/src/views/AdminsPage/Admins/Admins.tsx b/frontend/src/views/AdminsPage/Admins/Admins.tsx index 80de96229..fa2023f46 100644 --- a/frontend/src/views/AdminsPage/Admins/Admins.tsx +++ b/frontend/src/views/AdminsPage/Admins/Admins.tsx @@ -15,7 +15,11 @@ import { removeAdmin, removeAdminAndCoach, } from "../../../utils/api/users/admins"; -import { Error, SearchInput, SpinnerContainer } from "../../UsersPage/PendingRequests/styles"; +import { + Error, + SearchInput, + SpinnerContainer, +} from "../../../components/UsersComponents/PendingRequests/styles"; import { Typeahead } from "react-bootstrap-typeahead"; function AdminFilter(props: { diff --git a/frontend/src/views/UsersPage/Coaches/Coaches.tsx b/frontend/src/views/UsersPage/Coaches/Coaches.tsx deleted file mode 100644 index 9b56721b0..000000000 --- a/frontend/src/views/UsersPage/Coaches/Coaches.tsx +++ /dev/null @@ -1,302 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { CoachesTitle, CoachesContainer, CoachesTable, ModalContent } from "./styles"; -import { getUsers, User } from "../../../utils/api/users/users"; -import { Error, SearchInput, SpinnerContainer } from "../PendingRequests/styles"; -import { Button, Modal, Spinner } from "react-bootstrap"; -import { - getCoaches, - removeCoachFromAllEditions, - removeCoachFromEdition, - addCoachToEdition, -} from "../../../utils/api/users/coaches"; -import { AddAdminButton, ModalContentGreen } from "../../AdminsPage/Admins/styles"; -import { Typeahead } from "react-bootstrap-typeahead"; - -function CoachesHeader() { - return Coaches; -} - -function CoachFilter(props: { - search: boolean; - searchTerm: string; - filter: (key: string) => void; -}) { - return props.filter(e.target.value)} />; -} - -function AddCoach(props: { users: User[]; edition: string; refresh: () => void }) { - const [show, setShow] = useState(false); - const [selected, setSelected] = useState(undefined); - const [error, setError] = useState(""); - - const handleClose = () => { - setSelected(undefined); - setShow(false); - }; - const handleShow = () => setShow(true); - - async function addCoach(userId: number) { - try { - const added = await addCoachToEdition(userId, props.edition); - if (added) { - props.refresh(); - handleClose(); - } else { - setError("Something went wrong. Failed to add coach"); - } - } catch (error) { - setError("Something went wrong. Failed to add coach"); - } - } - - return ( - <> - - Add coach - - - - - - Add Coach - - - { - setSelected(selected[0] as User); - }} - id="non-coach-users" - options={props.users} - labelKey="name" - filterBy={["name"]} - emptyLabel="No users found." - placeholder={"user's name"} - /> - - - - - {error} - - - - - ); -} - -function RemoveCoach(props: { coach: User; edition: string; refresh: () => void }) { - const [show, setShow] = useState(false); - const [error, setError] = useState(""); - - const handleClose = () => setShow(false); - const handleShow = () => { - setShow(true); - setError(""); - }; - - async function removeCoach(userId: number, allEditions: boolean) { - try { - let removed; - if (allEditions) { - removed = await removeCoachFromAllEditions(userId); - } else { - removed = await removeCoachFromEdition(userId, props.edition); - } - - if (removed) { - props.refresh(); - handleClose(); - } else { - setError("Something went wrong. Failed to remove coach"); - } - } catch (error) { - setError("Something went wrong. Failed to remove coach"); - } - } - - return ( - <> - - - - - - Remove Coach - - -

                          {props.coach.name}

                          - {props.coach.email} -
                          - - - - - {error} - -
                          -
                          - - ); -} - -function CoachItem(props: { coach: User; edition: string; refresh: () => void }) { - return ( - - {props.coach.name} - {props.coach.email} - - - - - ); -} - -function CoachesList(props: { - coaches: User[]; - loading: boolean; - edition: string; - gotData: boolean; - refresh: () => void; -}) { - if (props.loading) { - return ( - - - - ); - } else if (props.coaches.length === 0) { - if (props.gotData) { - return
                          No coaches for this edition
                          ; - } else { - return null; - } - } - - const body = ( - - {props.coaches.map(coach => ( - - ))} - - ); - - return ( - - - - Name - Email - Remove from edition - - - {body} - - ); -} - -export default function Coaches(props: { edition: string }) { - const [allCoaches, setAllCoaches] = useState([]); - const [coaches, setCoaches] = useState([]); - const [users, setUsers] = useState([]); - const [gettingData, setGettingData] = useState(false); - const [searchTerm, setSearchTerm] = useState(""); - const [gotData, setGotData] = useState(false); - const [error, setError] = useState(""); - - async function getData() { - setGettingData(true); - setGotData(false); - try { - const coachResponse = await getCoaches(props.edition); - setAllCoaches(coachResponse.users); - setCoaches(coachResponse.users); - - const UsersResponse = await getUsers(); - const users = []; - for (const user of UsersResponse.users) { - if (!coachResponse.users.some(e => e.userId === user.userId)) { - users.push(user); - } - } - setUsers(users); - - setGotData(true); - setGettingData(false); - } catch (exception) { - setError("Oops, something went wrong..."); - setGettingData(false); - } - } - - useEffect(() => { - if (!gotData && !gettingData && !error) { - getData(); - } - }, [gotData, gettingData, error, getData]); - - const filter = (word: string) => { - setSearchTerm(word); - const newCoaches: User[] = []; - for (const coach of allCoaches) { - if (coach.name.toUpperCase().includes(word.toUpperCase())) { - newCoaches.push(coach); - } - } - setCoaches(newCoaches); - }; - - return ( - - - 0} - searchTerm={searchTerm} - filter={word => filter(word)} - /> - - - {error} - - ); -} diff --git a/frontend/src/views/UsersPage/Coaches/index.ts b/frontend/src/views/UsersPage/Coaches/index.ts deleted file mode 100644 index ef8ea8035..000000000 --- a/frontend/src/views/UsersPage/Coaches/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Coaches } from "./Coaches"; diff --git a/frontend/src/views/UsersPage/InviteUser/index.ts b/frontend/src/views/UsersPage/InviteUser/index.ts deleted file mode 100644 index f268c1378..000000000 --- a/frontend/src/views/UsersPage/InviteUser/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as InviteUser } from "./InviteUser"; diff --git a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx b/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx deleted file mode 100644 index 82eb11e81..000000000 --- a/frontend/src/views/UsersPage/PendingRequests/PendingRequests.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import React, { useEffect, useState } from "react"; -import Collapsible from "react-collapsible"; -import { - RequestHeaderTitle, - RequestHeaderDiv, - OpenArrow, - ClosedArrow, - RequestsTable, - PendingRequestsContainer, - AcceptButton, - RejectButton, - SpinnerContainer, - SearchInput, - AcceptRejectTh, - Error, -} from "./styles"; -import { - acceptRequest, - getRequests, - rejectRequest, - Request, -} from "../../../utils/api/users/requests"; -import { Spinner } from "react-bootstrap"; - -function Arrow(props: { open: boolean }) { - if (props.open) { - return ; - } else { - return ; - } -} - -function RequestHeader(props: { open: boolean }) { - return ( - - Requests - - - ); -} - -function RequestFilter(props: { - searchTerm: string; - filter: (key: string) => void; - show: boolean; -}) { - if (props.show) { - return ( - props.filter(e.target.value)} /> - ); - } else { - return null; - } -} - -function AcceptReject(props: { requestId: number }) { - return ( -
                          - acceptRequest(props.requestId)}>Accept - rejectRequest(props.requestId)}>Reject -
                          - ); -} - -function RequestItem(props: { request: Request }) { - return ( - - {props.request.user.name} - {props.request.user.email} - - - - - ); -} - -function RequestsList(props: { requests: Request[]; loading: boolean; gotData: boolean }) { - if (props.loading) { - return ( - - - - ); - } else if (props.requests.length === 0) { - if (props.gotData) { - return
                          No requests
                          ; - } else { - return null; - } - } - - const body = ( - - {props.requests.map(request => ( - - ))} - - ); - - return ( - - - - Name - Email - Accept/Reject - - - {body} - - ); -} - -export default function PendingRequests(props: { edition: string }) { - const [allRequests, setAllRequests] = useState([]); - const [requests, setRequests] = useState([]); - const [gettingRequests, setGettingRequests] = useState(false); - const [searchTerm, setSearchTerm] = useState(""); - const [gotData, setGotData] = useState(false); - const [open, setOpen] = useState(false); - const [error, setError] = useState(""); - - async function getData() { - try { - const response = await getRequests(props.edition); - setAllRequests(response.requests); - setRequests(response.requests); - setGotData(true); - setGettingRequests(false); - } catch (exception) { - setError("Oops, something went wrong..."); - setGettingRequests(false); - } - } - - useEffect(() => { - if (!gotData && !gettingRequests && !error) { - setGettingRequests(true); - getData(); - } - }, [gotData, gettingRequests, error, getData]); - - const filter = (word: string) => { - setSearchTerm(word); - const newRequests: Request[] = []; - for (const request of allRequests) { - if (request.user.name.toUpperCase().includes(word.toUpperCase())) { - newRequests.push(request); - } - } - setRequests(newRequests); - }; - - return ( - - } - onOpening={() => setOpen(true)} - onClosing={() => setOpen(false)} - > - filter(word)} - show={allRequests.length > 0} - /> - - {error} - - - ); -} diff --git a/frontend/src/views/UsersPage/PendingRequests/index.ts b/frontend/src/views/UsersPage/PendingRequests/index.ts deleted file mode 100644 index 6e87caa6b..000000000 --- a/frontend/src/views/UsersPage/PendingRequests/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as PendingRequests } from "./PendingRequests"; diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index bd267f371..0ef288f2e 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -1,10 +1,13 @@ import React from "react"; -import { InviteUser } from "./InviteUser"; -import { PendingRequests } from "./PendingRequests"; -import { Coaches } from "./Coaches"; import { useParams, useNavigate } from "react-router-dom"; import { UsersPageDiv, AdminsButton, UsersHeader } from "./styles"; +import { Coaches } from "../../components/UsersComponents/Coaches"; +import { InviteUser } from "../../components/UsersComponents/InviteUser"; +import { PendingRequests } from "../../components/UsersComponents/PendingRequests"; +/** + * Page for admins to manage coach and admin settings. + */ function UsersPage() { const params = useParams(); const navigate = useNavigate(); From 5be686c7af35c58f3abab21ff832e5d066f80d29 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Mon, 4 Apr 2022 12:02:36 +0200 Subject: [PATCH 224/536] Add docstrings --- frontend/docs/assets/search.js | 2 +- frontend/docs/enums/data.Enums.Role.html | 2 +- .../docs/enums/data.Enums.StorageKey.html | 2 +- .../interfaces/contexts.AuthContextState.html | 2 +- .../docs/interfaces/data.Interfaces.User.html | 2 +- frontend/docs/modules/App.html | 2 +- frontend/docs/modules/Router.html | 2 +- .../modules/components.LoginComponents.html | 8 +++--- .../components.RegisterComponents.html | 14 +++++----- frontend/docs/modules/components.html | 10 +++---- frontend/docs/modules/contexts.html | 2 +- frontend/docs/modules/utils.Api.html | 4 +-- frontend/docs/modules/utils.LocalStorage.html | 4 +-- frontend/docs/modules/views.Errors.html | 4 +-- frontend/docs/modules/views.html | 10 +++---- .../UsersComponents/Coaches/Coaches.tsx | 25 ++++++++++------- .../Coaches/CoachesComponents/AddCoach.tsx | 7 +++++ .../Coaches/CoachesComponents/CoachList.tsx | 12 +++++++-- .../CoachesComponents/CoachListItem.tsx | 7 +++++ .../Coaches/CoachesComponents/RemoveCoach.tsx | 7 +++++ .../Coaches/CoachesComponents/index.ts | 2 +- .../UsersComponents/InviteUser/InviteUser.tsx | 19 +++++++------ .../InviteUserComponents/ButtonsDiv.tsx | 5 ++++ .../InviteUserComponents/ErrorDiv.tsx | 4 +++ .../InviteUserComponents/LinkDiv.tsx | 10 ------- .../InviteUser/InviteUserComponents/index.ts | 1 - .../UsersComponents/InviteUser/styles.ts | 4 --- .../PendingRequests/PendingRequests.tsx | 27 ++++++++++--------- .../AcceptReject.tsx | 4 +++ .../RequestFilter.tsx | 6 +++++ .../PendingRequestsComponents/RequestList.tsx | 6 +++++ .../RequestListItem.tsx | 5 ++++ .../RequestsHeader.tsx | 8 ++++++ 33 files changed, 145 insertions(+), 84 deletions(-) delete mode 100644 frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/LinkDiv.tsx diff --git a/frontend/docs/assets/search.js b/frontend/docs/assets/search.js index d01761199..47b53be32 100644 --- a/frontend/docs/assets/search.js +++ b/frontend/docs/assets/search.js @@ -1 +1 @@ -window.searchData = JSON.parse("{\"kinds\":{\"2\":\"Module\",\"4\":\"Namespace\",\"8\":\"Enumeration\",\"16\":\"Enumeration member\",\"64\":\"Function\",\"256\":\"Interface\",\"1024\":\"Property\",\"2048\":\"Method\"},\"rows\":[{\"id\":0,\"kind\":2,\"name\":\"App\",\"url\":\"modules/App.html\",\"classes\":\"tsd-kind-module\"},{\"id\":1,\"kind\":64,\"name\":\"default\",\"url\":\"modules/App.html#default\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"App\"},{\"id\":2,\"kind\":2,\"name\":\"Router\",\"url\":\"modules/Router.html\",\"classes\":\"tsd-kind-module\"},{\"id\":3,\"kind\":64,\"name\":\"default\",\"url\":\"modules/Router.html#default\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"Router\"},{\"id\":4,\"kind\":2,\"name\":\"components\",\"url\":\"modules/components.html\",\"classes\":\"tsd-kind-module\"},{\"id\":5,\"kind\":2,\"name\":\"contexts\",\"url\":\"modules/contexts.html\",\"classes\":\"tsd-kind-module\"},{\"id\":6,\"kind\":2,\"name\":\"data\",\"url\":\"modules/data.html\",\"classes\":\"tsd-kind-module\"},{\"id\":7,\"kind\":2,\"name\":\"utils\",\"url\":\"modules/utils.html\",\"classes\":\"tsd-kind-module\"},{\"id\":8,\"kind\":2,\"name\":\"views\",\"url\":\"modules/views.html\",\"classes\":\"tsd-kind-module\"},{\"id\":9,\"kind\":64,\"name\":\"AdminRoute\",\"url\":\"modules/components.html#AdminRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":10,\"kind\":64,\"name\":\"Footer\",\"url\":\"modules/components.html#Footer\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":11,\"kind\":4,\"name\":\"LoginComponents\",\"url\":\"modules/components.LoginComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":12,\"kind\":64,\"name\":\"WelcomeText\",\"url\":\"modules/components.LoginComponents.html#WelcomeText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":13,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/components.LoginComponents.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":14,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/components.LoginComponents.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":15,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/components.LoginComponents.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":16,\"kind\":64,\"name\":\"NavBar\",\"url\":\"modules/components.html#NavBar\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":17,\"kind\":64,\"name\":\"OSOCLetters\",\"url\":\"modules/components.html#OSOCLetters\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":18,\"kind\":64,\"name\":\"PrivateRoute\",\"url\":\"modules/components.html#PrivateRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":19,\"kind\":4,\"name\":\"RegisterComponents\",\"url\":\"modules/components.RegisterComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":20,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/components.RegisterComponents.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":21,\"kind\":64,\"name\":\"Name\",\"url\":\"modules/components.RegisterComponents.html#Name\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":22,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/components.RegisterComponents.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":23,\"kind\":64,\"name\":\"ConfirmPassword\",\"url\":\"modules/components.RegisterComponents.html#ConfirmPassword\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":24,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/components.RegisterComponents.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":25,\"kind\":64,\"name\":\"InfoText\",\"url\":\"modules/components.RegisterComponents.html#InfoText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":26,\"kind\":64,\"name\":\"BadInviteLink\",\"url\":\"modules/components.RegisterComponents.html#BadInviteLink\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":27,\"kind\":4,\"name\":\"UsersComponents\",\"url\":\"modules/components.UsersComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":28,\"kind\":4,\"name\":\"Coaches\",\"url\":\"modules/components.UsersComponents.Coaches.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents\"},{\"id\":29,\"kind\":64,\"name\":\"Coaches\",\"url\":\"modules/components.UsersComponents.Coaches.html#Coaches\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches\"},{\"id\":30,\"kind\":4,\"name\":\"CoachesComponents\",\"url\":\"modules/components.UsersComponents.Coaches.CoachesComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches\"},{\"id\":31,\"kind\":64,\"name\":\"AddCoach\",\"url\":\"modules/components.UsersComponents.Coaches.CoachesComponents.html#AddCoach\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches.CoachesComponents\"},{\"id\":32,\"kind\":64,\"name\":\"CoachItem\",\"url\":\"modules/components.UsersComponents.Coaches.CoachesComponents.html#CoachItem\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches.CoachesComponents\"},{\"id\":33,\"kind\":64,\"name\":\"CoachList\",\"url\":\"modules/components.UsersComponents.Coaches.CoachesComponents.html#CoachList\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches.CoachesComponents\"},{\"id\":34,\"kind\":64,\"name\":\"RemoveCoach\",\"url\":\"modules/components.UsersComponents.Coaches.CoachesComponents.html#RemoveCoach\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches.CoachesComponents\"},{\"id\":35,\"kind\":4,\"name\":\"InviteUser\",\"url\":\"modules/components.UsersComponents.InviteUser.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents\"},{\"id\":36,\"kind\":64,\"name\":\"InviteUser\",\"url\":\"modules/components.UsersComponents.InviteUser.html#InviteUser\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.InviteUser\"},{\"id\":37,\"kind\":4,\"name\":\"InviteUserComponents\",\"url\":\"modules/components.UsersComponents.InviteUser.InviteUserComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.InviteUser\"},{\"id\":38,\"kind\":64,\"name\":\"ButtonsDiv\",\"url\":\"modules/components.UsersComponents.InviteUser.InviteUserComponents.html#ButtonsDiv\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.InviteUser.InviteUserComponents\"},{\"id\":39,\"kind\":64,\"name\":\"ErrorDiv\",\"url\":\"modules/components.UsersComponents.InviteUser.InviteUserComponents.html#ErrorDiv\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.InviteUser.InviteUserComponents\"},{\"id\":40,\"kind\":64,\"name\":\"LinkDiv\",\"url\":\"modules/components.UsersComponents.InviteUser.InviteUserComponents.html#LinkDiv\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.InviteUser.InviteUserComponents\"},{\"id\":41,\"kind\":4,\"name\":\"PendingRequests\",\"url\":\"modules/components.UsersComponents.PendingRequests.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents\"},{\"id\":42,\"kind\":64,\"name\":\"PendingRequests\",\"url\":\"modules/components.UsersComponents.PendingRequests.html#PendingRequests\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests\"},{\"id\":43,\"kind\":4,\"name\":\"PendingRequestsComponents\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests\"},{\"id\":44,\"kind\":64,\"name\":\"AcceptReject\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html#AcceptReject\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests.PendingRequestsComponents\"},{\"id\":45,\"kind\":64,\"name\":\"RequestFilter\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html#RequestFilter\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests.PendingRequestsComponents\"},{\"id\":46,\"kind\":64,\"name\":\"RequestListItem\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html#RequestListItem\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests.PendingRequestsComponents\"},{\"id\":47,\"kind\":64,\"name\":\"RequestList\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html#RequestList\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests.PendingRequestsComponents\"},{\"id\":48,\"kind\":64,\"name\":\"RequestsHeader\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html#RequestsHeader\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests.PendingRequestsComponents\"},{\"id\":49,\"kind\":256,\"name\":\"AuthContextState\",\"url\":\"interfaces/contexts.AuthContextState.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-module\",\"parent\":\"contexts\"},{\"id\":50,\"kind\":1024,\"name\":\"isLoggedIn\",\"url\":\"interfaces/contexts.AuthContextState.html#isLoggedIn\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":51,\"kind\":2048,\"name\":\"setIsLoggedIn\",\"url\":\"interfaces/contexts.AuthContextState.html#setIsLoggedIn\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":52,\"kind\":1024,\"name\":\"role\",\"url\":\"interfaces/contexts.AuthContextState.html#role\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":53,\"kind\":2048,\"name\":\"setRole\",\"url\":\"interfaces/contexts.AuthContextState.html#setRole\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":54,\"kind\":1024,\"name\":\"token\",\"url\":\"interfaces/contexts.AuthContextState.html#token\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":55,\"kind\":2048,\"name\":\"setToken\",\"url\":\"interfaces/contexts.AuthContextState.html#setToken\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":56,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/contexts.AuthContextState.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":57,\"kind\":2048,\"name\":\"setEditions\",\"url\":\"interfaces/contexts.AuthContextState.html#setEditions\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":58,\"kind\":64,\"name\":\"AuthProvider\",\"url\":\"modules/contexts.html#AuthProvider\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"contexts\"},{\"id\":59,\"kind\":4,\"name\":\"Enums\",\"url\":\"modules/data.Enums.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"data\"},{\"id\":60,\"kind\":8,\"name\":\"StorageKey\",\"url\":\"enums/data.Enums.StorageKey.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"data.Enums\"},{\"id\":61,\"kind\":16,\"name\":\"BEARER_TOKEN\",\"url\":\"enums/data.Enums.StorageKey.html#BEARER_TOKEN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.StorageKey\"},{\"id\":62,\"kind\":8,\"name\":\"Role\",\"url\":\"enums/data.Enums.Role.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"data.Enums\"},{\"id\":63,\"kind\":16,\"name\":\"ADMIN\",\"url\":\"enums/data.Enums.Role.html#ADMIN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.Role\"},{\"id\":64,\"kind\":16,\"name\":\"COACH\",\"url\":\"enums/data.Enums.Role.html#COACH\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.Role\"},{\"id\":65,\"kind\":4,\"name\":\"Interfaces\",\"url\":\"modules/data.Interfaces.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"data\"},{\"id\":66,\"kind\":256,\"name\":\"User\",\"url\":\"interfaces/data.Interfaces.User.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"data.Interfaces\"},{\"id\":67,\"kind\":1024,\"name\":\"userId\",\"url\":\"interfaces/data.Interfaces.User.html#userId\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":68,\"kind\":1024,\"name\":\"name\",\"url\":\"interfaces/data.Interfaces.User.html#name\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":69,\"kind\":1024,\"name\":\"admin\",\"url\":\"interfaces/data.Interfaces.User.html#admin\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":70,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/data.Interfaces.User.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":71,\"kind\":4,\"name\":\"Api\",\"url\":\"modules/utils.Api.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"utils\"},{\"id\":72,\"kind\":64,\"name\":\"validateRegistrationUrl\",\"url\":\"modules/utils.Api.html#validateRegistrationUrl\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api\"},{\"id\":73,\"kind\":64,\"name\":\"setBearerToken\",\"url\":\"modules/utils.Api.html#setBearerToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api\"},{\"id\":74,\"kind\":4,\"name\":\"Users\",\"url\":\"modules/utils.Api.Users.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"utils.Api\"},{\"id\":75,\"kind\":4,\"name\":\"Admins\",\"url\":\"modules/utils.Api.Users.Admins.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users\"},{\"id\":76,\"kind\":64,\"name\":\"getAdmins\",\"url\":\"modules/utils.Api.Users.Admins.html#getAdmins\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Admins\"},{\"id\":77,\"kind\":64,\"name\":\"addAdmin\",\"url\":\"modules/utils.Api.Users.Admins.html#addAdmin\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Admins\"},{\"id\":78,\"kind\":64,\"name\":\"removeAdmin\",\"url\":\"modules/utils.Api.Users.Admins.html#removeAdmin\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Admins\"},{\"id\":79,\"kind\":64,\"name\":\"removeAdminAndCoach\",\"url\":\"modules/utils.Api.Users.Admins.html#removeAdminAndCoach\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Admins\"},{\"id\":80,\"kind\":4,\"name\":\"Coaches\",\"url\":\"modules/utils.Api.Users.Coaches.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users\"},{\"id\":81,\"kind\":64,\"name\":\"getCoaches\",\"url\":\"modules/utils.Api.Users.Coaches.html#getCoaches\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Coaches\"},{\"id\":82,\"kind\":64,\"name\":\"removeCoachFromEdition\",\"url\":\"modules/utils.Api.Users.Coaches.html#removeCoachFromEdition\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Coaches\"},{\"id\":83,\"kind\":64,\"name\":\"removeCoachFromAllEditions\",\"url\":\"modules/utils.Api.Users.Coaches.html#removeCoachFromAllEditions\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Coaches\"},{\"id\":84,\"kind\":64,\"name\":\"addCoachToEdition\",\"url\":\"modules/utils.Api.Users.Coaches.html#addCoachToEdition\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Coaches\"},{\"id\":85,\"kind\":4,\"name\":\"Requests\",\"url\":\"modules/utils.Api.Users.Requests.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users\"},{\"id\":86,\"kind\":64,\"name\":\"getRequests\",\"url\":\"modules/utils.Api.Users.Requests.html#getRequests\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Requests\"},{\"id\":87,\"kind\":64,\"name\":\"acceptRequest\",\"url\":\"modules/utils.Api.Users.Requests.html#acceptRequest\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Requests\"},{\"id\":88,\"kind\":64,\"name\":\"rejectRequest\",\"url\":\"modules/utils.Api.Users.Requests.html#rejectRequest\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Requests\"},{\"id\":89,\"kind\":256,\"name\":\"Request\",\"url\":\"interfaces/utils.Api.Users.Requests.Request.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Requests\"},{\"id\":90,\"kind\":1024,\"name\":\"requestId\",\"url\":\"interfaces/utils.Api.Users.Requests.Request.html#requestId\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Requests.Request\"},{\"id\":91,\"kind\":1024,\"name\":\"user\",\"url\":\"interfaces/utils.Api.Users.Requests.Request.html#user\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Requests.Request\"},{\"id\":92,\"kind\":256,\"name\":\"GetRequestsResponse\",\"url\":\"interfaces/utils.Api.Users.Requests.GetRequestsResponse.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Requests\"},{\"id\":93,\"kind\":1024,\"name\":\"requests\",\"url\":\"interfaces/utils.Api.Users.Requests.GetRequestsResponse.html#requests\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Requests.GetRequestsResponse\"},{\"id\":94,\"kind\":4,\"name\":\"Users\",\"url\":\"modules/utils.Api.Users.Users.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users\"},{\"id\":95,\"kind\":64,\"name\":\"getInviteLink\",\"url\":\"modules/utils.Api.Users.Users.html#getInviteLink\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Users\"},{\"id\":96,\"kind\":64,\"name\":\"getUsers\",\"url\":\"modules/utils.Api.Users.Users.html#getUsers\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Users\"},{\"id\":97,\"kind\":256,\"name\":\"User\",\"url\":\"interfaces/utils.Api.Users.Users.User.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Users\"},{\"id\":98,\"kind\":1024,\"name\":\"userId\",\"url\":\"interfaces/utils.Api.Users.Users.User.html#userId\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.User\"},{\"id\":99,\"kind\":1024,\"name\":\"name\",\"url\":\"interfaces/utils.Api.Users.Users.User.html#name\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.User\"},{\"id\":100,\"kind\":1024,\"name\":\"email\",\"url\":\"interfaces/utils.Api.Users.Users.User.html#email\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.User\"},{\"id\":101,\"kind\":1024,\"name\":\"admin\",\"url\":\"interfaces/utils.Api.Users.Users.User.html#admin\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.User\"},{\"id\":102,\"kind\":256,\"name\":\"UsersList\",\"url\":\"interfaces/utils.Api.Users.Users.UsersList.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Users\"},{\"id\":103,\"kind\":1024,\"name\":\"users\",\"url\":\"interfaces/utils.Api.Users.Users.UsersList.html#users\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.UsersList\"},{\"id\":104,\"kind\":256,\"name\":\"MailTo\",\"url\":\"interfaces/utils.Api.Users.Users.MailTo.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Users\"},{\"id\":105,\"kind\":1024,\"name\":\"mailTo\",\"url\":\"interfaces/utils.Api.Users.Users.MailTo.html#mailTo\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.MailTo\"},{\"id\":106,\"kind\":1024,\"name\":\"link\",\"url\":\"interfaces/utils.Api.Users.Users.MailTo.html#link\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.MailTo\"},{\"id\":107,\"kind\":4,\"name\":\"LocalStorage\",\"url\":\"modules/utils.LocalStorage.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"utils\"},{\"id\":108,\"kind\":64,\"name\":\"getToken\",\"url\":\"modules/utils.LocalStorage.html#getToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.LocalStorage\"},{\"id\":109,\"kind\":64,\"name\":\"setToken\",\"url\":\"modules/utils.LocalStorage.html#setToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.LocalStorage\"},{\"id\":110,\"kind\":4,\"name\":\"Errors\",\"url\":\"modules/views.Errors.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":111,\"kind\":64,\"name\":\"ForbiddenPage\",\"url\":\"modules/views.Errors.html#ForbiddenPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"views.Errors\"},{\"id\":112,\"kind\":64,\"name\":\"NotFoundPage\",\"url\":\"modules/views.Errors.html#NotFoundPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"views.Errors\"},{\"id\":113,\"kind\":64,\"name\":\"LoginPage\",\"url\":\"modules/views.html#LoginPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":114,\"kind\":64,\"name\":\"PendingPage\",\"url\":\"modules/views.html#PendingPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":115,\"kind\":64,\"name\":\"ProjectsPage\",\"url\":\"modules/views.html#ProjectsPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":116,\"kind\":64,\"name\":\"RegisterPage\",\"url\":\"modules/views.html#RegisterPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":117,\"kind\":64,\"name\":\"StudentsPage\",\"url\":\"modules/views.html#StudentsPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":118,\"kind\":64,\"name\":\"UsersPage\",\"url\":\"modules/views.html#UsersPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":119,\"kind\":64,\"name\":\"VerifyingTokenPage\",\"url\":\"modules/views.html#VerifyingTokenPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"parent\"],\"fieldVectors\":[[\"name/0\",[0,38.795]],[\"parent/0\",[]],[\"name/1\",[1,38.795]],[\"parent/1\",[0,3.784]],[\"name/2\",[2,38.795]],[\"parent/2\",[]],[\"name/3\",[1,38.795]],[\"parent/3\",[2,3.784]],[\"name/4\",[3,25.445]],[\"parent/4\",[]],[\"name/5\",[4,35.43]],[\"parent/5\",[]],[\"name/6\",[5,35.43]],[\"parent/6\",[]],[\"name/7\",[6,35.43]],[\"parent/7\",[]],[\"name/8\",[7,25.445]],[\"parent/8\",[]],[\"name/9\",[8,43.903]],[\"parent/9\",[3,2.482]],[\"name/10\",[9,43.903]],[\"parent/10\",[3,2.482]],[\"name/11\",[10,43.903]],[\"parent/11\",[3,2.482]],[\"name/12\",[11,43.903]],[\"parent/12\",[12,3.21]],[\"name/13\",[13,38.795]],[\"parent/13\",[12,3.21]],[\"name/14\",[14,38.795]],[\"parent/14\",[12,3.21]],[\"name/15\",[15,35.43]],[\"parent/15\",[12,3.21]],[\"name/16\",[16,43.903]],[\"parent/16\",[3,2.482]],[\"name/17\",[17,43.903]],[\"parent/17\",[3,2.482]],[\"name/18\",[18,43.903]],[\"parent/18\",[3,2.482]],[\"name/19\",[19,43.903]],[\"parent/19\",[3,2.482]],[\"name/20\",[15,35.43]],[\"parent/20\",[20,2.712]],[\"name/21\",[21,35.43]],[\"parent/21\",[20,2.712]],[\"name/22\",[14,38.795]],[\"parent/22\",[20,2.712]],[\"name/23\",[22,43.903]],[\"parent/23\",[20,2.712]],[\"name/24\",[13,38.795]],[\"parent/24\",[20,2.712]],[\"name/25\",[23,43.903]],[\"parent/25\",[20,2.712]],[\"name/26\",[24,43.903]],[\"parent/26\",[20,2.712]],[\"name/27\",[25,43.903]],[\"parent/27\",[3,2.482]],[\"name/28\",[26,35.43]],[\"parent/28\",[27,3.455]],[\"name/29\",[26,35.43]],[\"parent/29\",[28,3.784]],[\"name/30\",[29,43.903]],[\"parent/30\",[28,3.784]],[\"name/31\",[30,43.903]],[\"parent/31\",[31,3.21]],[\"name/32\",[32,43.903]],[\"parent/32\",[31,3.21]],[\"name/33\",[33,43.903]],[\"parent/33\",[31,3.21]],[\"name/34\",[34,43.903]],[\"parent/34\",[31,3.21]],[\"name/35\",[35,38.795]],[\"parent/35\",[27,3.455]],[\"name/36\",[35,38.795]],[\"parent/36\",[36,3.784]],[\"name/37\",[37,43.903]],[\"parent/37\",[36,3.784]],[\"name/38\",[38,43.903]],[\"parent/38\",[39,3.455]],[\"name/39\",[40,43.903]],[\"parent/39\",[39,3.455]],[\"name/40\",[41,43.903]],[\"parent/40\",[39,3.455]],[\"name/41\",[42,38.795]],[\"parent/41\",[27,3.455]],[\"name/42\",[42,38.795]],[\"parent/42\",[43,3.784]],[\"name/43\",[44,43.903]],[\"parent/43\",[43,3.784]],[\"name/44\",[45,43.903]],[\"parent/44\",[46,3.015]],[\"name/45\",[47,43.903]],[\"parent/45\",[46,3.015]],[\"name/46\",[48,43.903]],[\"parent/46\",[46,3.015]],[\"name/47\",[49,43.903]],[\"parent/47\",[46,3.015]],[\"name/48\",[50,43.903]],[\"parent/48\",[46,3.015]],[\"name/49\",[51,43.903]],[\"parent/49\",[4,3.455]],[\"name/50\",[52,43.903]],[\"parent/50\",[53,2.59]],[\"name/51\",[54,43.903]],[\"parent/51\",[53,2.59]],[\"name/52\",[55,38.795]],[\"parent/52\",[53,2.59]],[\"name/53\",[56,43.903]],[\"parent/53\",[53,2.59]],[\"name/54\",[57,43.903]],[\"parent/54\",[53,2.59]],[\"name/55\",[58,38.795]],[\"parent/55\",[53,2.59]],[\"name/56\",[59,38.795]],[\"parent/56\",[53,2.59]],[\"name/57\",[60,43.903]],[\"parent/57\",[53,2.59]],[\"name/58\",[61,43.903]],[\"parent/58\",[4,3.455]],[\"name/59\",[62,43.903]],[\"parent/59\",[5,3.455]],[\"name/60\",[63,43.903]],[\"parent/60\",[64,3.784]],[\"name/61\",[65,43.903]],[\"parent/61\",[66,4.282]],[\"name/62\",[55,38.795]],[\"parent/62\",[64,3.784]],[\"name/63\",[67,35.43]],[\"parent/63\",[68,3.784]],[\"name/64\",[69,43.903]],[\"parent/64\",[68,3.784]],[\"name/65\",[70,43.903]],[\"parent/65\",[5,3.455]],[\"name/66\",[71,35.43]],[\"parent/66\",[72,4.282]],[\"name/67\",[73,38.795]],[\"parent/67\",[74,3.21]],[\"name/68\",[21,35.43]],[\"parent/68\",[74,3.21]],[\"name/69\",[67,35.43]],[\"parent/69\",[74,3.21]],[\"name/70\",[59,38.795]],[\"parent/70\",[74,3.21]],[\"name/71\",[75,43.903]],[\"parent/71\",[6,3.455]],[\"name/72\",[76,43.903]],[\"parent/72\",[77,3.455]],[\"name/73\",[78,43.903]],[\"parent/73\",[77,3.455]],[\"name/74\",[79,35.43]],[\"parent/74\",[77,3.455]],[\"name/75\",[80,43.903]],[\"parent/75\",[81,3.21]],[\"name/76\",[82,43.903]],[\"parent/76\",[83,3.21]],[\"name/77\",[84,43.903]],[\"parent/77\",[83,3.21]],[\"name/78\",[85,43.903]],[\"parent/78\",[83,3.21]],[\"name/79\",[86,43.903]],[\"parent/79\",[83,3.21]],[\"name/80\",[26,35.43]],[\"parent/80\",[81,3.21]],[\"name/81\",[87,43.903]],[\"parent/81\",[88,3.21]],[\"name/82\",[89,43.903]],[\"parent/82\",[88,3.21]],[\"name/83\",[90,43.903]],[\"parent/83\",[88,3.21]],[\"name/84\",[91,43.903]],[\"parent/84\",[88,3.21]],[\"name/85\",[92,38.795]],[\"parent/85\",[81,3.21]],[\"name/86\",[93,43.903]],[\"parent/86\",[94,3.015]],[\"name/87\",[95,43.903]],[\"parent/87\",[94,3.015]],[\"name/88\",[96,43.903]],[\"parent/88\",[94,3.015]],[\"name/89\",[97,43.903]],[\"parent/89\",[94,3.015]],[\"name/90\",[98,43.903]],[\"parent/90\",[99,3.784]],[\"name/91\",[71,35.43]],[\"parent/91\",[99,3.784]],[\"name/92\",[100,43.903]],[\"parent/92\",[94,3.015]],[\"name/93\",[92,38.795]],[\"parent/93\",[101,4.282]],[\"name/94\",[79,35.43]],[\"parent/94\",[81,3.21]],[\"name/95\",[102,43.903]],[\"parent/95\",[103,3.015]],[\"name/96\",[104,43.903]],[\"parent/96\",[103,3.015]],[\"name/97\",[71,35.43]],[\"parent/97\",[103,3.015]],[\"name/98\",[73,38.795]],[\"parent/98\",[105,3.21]],[\"name/99\",[21,35.43]],[\"parent/99\",[105,3.21]],[\"name/100\",[15,35.43]],[\"parent/100\",[105,3.21]],[\"name/101\",[67,35.43]],[\"parent/101\",[105,3.21]],[\"name/102\",[106,43.903]],[\"parent/102\",[103,3.015]],[\"name/103\",[79,35.43]],[\"parent/103\",[107,4.282]],[\"name/104\",[108,38.795]],[\"parent/104\",[103,3.015]],[\"name/105\",[108,38.795]],[\"parent/105\",[109,3.784]],[\"name/106\",[110,43.903]],[\"parent/106\",[109,3.784]],[\"name/107\",[111,43.903]],[\"parent/107\",[6,3.455]],[\"name/108\",[112,43.903]],[\"parent/108\",[113,3.784]],[\"name/109\",[58,38.795]],[\"parent/109\",[113,3.784]],[\"name/110\",[114,43.903]],[\"parent/110\",[7,2.482]],[\"name/111\",[115,43.903]],[\"parent/111\",[116,3.784]],[\"name/112\",[117,43.903]],[\"parent/112\",[116,3.784]],[\"name/113\",[118,43.903]],[\"parent/113\",[7,2.482]],[\"name/114\",[119,43.903]],[\"parent/114\",[7,2.482]],[\"name/115\",[120,43.903]],[\"parent/115\",[7,2.482]],[\"name/116\",[121,43.903]],[\"parent/116\",[7,2.482]],[\"name/117\",[122,43.903]],[\"parent/117\",[7,2.482]],[\"name/118\",[123,43.903]],[\"parent/118\",[7,2.482]],[\"name/119\",[124,43.903]],[\"parent/119\",[7,2.482]]],\"invertedIndex\":[[\"acceptreject\",{\"_index\":45,\"name\":{\"44\":{}},\"parent\":{}}],[\"acceptrequest\",{\"_index\":95,\"name\":{\"87\":{}},\"parent\":{}}],[\"addadmin\",{\"_index\":84,\"name\":{\"77\":{}},\"parent\":{}}],[\"addcoach\",{\"_index\":30,\"name\":{\"31\":{}},\"parent\":{}}],[\"addcoachtoedition\",{\"_index\":91,\"name\":{\"84\":{}},\"parent\":{}}],[\"admin\",{\"_index\":67,\"name\":{\"63\":{},\"69\":{},\"101\":{}},\"parent\":{}}],[\"adminroute\",{\"_index\":8,\"name\":{\"9\":{}},\"parent\":{}}],[\"admins\",{\"_index\":80,\"name\":{\"75\":{}},\"parent\":{}}],[\"api\",{\"_index\":75,\"name\":{\"71\":{}},\"parent\":{}}],[\"app\",{\"_index\":0,\"name\":{\"0\":{}},\"parent\":{\"1\":{}}}],[\"authcontextstate\",{\"_index\":51,\"name\":{\"49\":{}},\"parent\":{}}],[\"authprovider\",{\"_index\":61,\"name\":{\"58\":{}},\"parent\":{}}],[\"badinvitelink\",{\"_index\":24,\"name\":{\"26\":{}},\"parent\":{}}],[\"bearer_token\",{\"_index\":65,\"name\":{\"61\":{}},\"parent\":{}}],[\"buttonsdiv\",{\"_index\":38,\"name\":{\"38\":{}},\"parent\":{}}],[\"coach\",{\"_index\":69,\"name\":{\"64\":{}},\"parent\":{}}],[\"coaches\",{\"_index\":26,\"name\":{\"28\":{},\"29\":{},\"80\":{}},\"parent\":{}}],[\"coachescomponents\",{\"_index\":29,\"name\":{\"30\":{}},\"parent\":{}}],[\"coachitem\",{\"_index\":32,\"name\":{\"32\":{}},\"parent\":{}}],[\"coachlist\",{\"_index\":33,\"name\":{\"33\":{}},\"parent\":{}}],[\"components\",{\"_index\":3,\"name\":{\"4\":{}},\"parent\":{\"9\":{},\"10\":{},\"11\":{},\"16\":{},\"17\":{},\"18\":{},\"19\":{},\"27\":{}}}],[\"components.logincomponents\",{\"_index\":12,\"name\":{},\"parent\":{\"12\":{},\"13\":{},\"14\":{},\"15\":{}}}],[\"components.registercomponents\",{\"_index\":20,\"name\":{},\"parent\":{\"20\":{},\"21\":{},\"22\":{},\"23\":{},\"24\":{},\"25\":{},\"26\":{}}}],[\"components.userscomponents\",{\"_index\":27,\"name\":{},\"parent\":{\"28\":{},\"35\":{},\"41\":{}}}],[\"components.userscomponents.coaches\",{\"_index\":28,\"name\":{},\"parent\":{\"29\":{},\"30\":{}}}],[\"components.userscomponents.coaches.coachescomponents\",{\"_index\":31,\"name\":{},\"parent\":{\"31\":{},\"32\":{},\"33\":{},\"34\":{}}}],[\"components.userscomponents.inviteuser\",{\"_index\":36,\"name\":{},\"parent\":{\"36\":{},\"37\":{}}}],[\"components.userscomponents.inviteuser.inviteusercomponents\",{\"_index\":39,\"name\":{},\"parent\":{\"38\":{},\"39\":{},\"40\":{}}}],[\"components.userscomponents.pendingrequests\",{\"_index\":43,\"name\":{},\"parent\":{\"42\":{},\"43\":{}}}],[\"components.userscomponents.pendingrequests.pendingrequestscomponents\",{\"_index\":46,\"name\":{},\"parent\":{\"44\":{},\"45\":{},\"46\":{},\"47\":{},\"48\":{}}}],[\"confirmpassword\",{\"_index\":22,\"name\":{\"23\":{}},\"parent\":{}}],[\"contexts\",{\"_index\":4,\"name\":{\"5\":{}},\"parent\":{\"49\":{},\"58\":{}}}],[\"contexts.authcontextstate\",{\"_index\":53,\"name\":{},\"parent\":{\"50\":{},\"51\":{},\"52\":{},\"53\":{},\"54\":{},\"55\":{},\"56\":{},\"57\":{}}}],[\"data\",{\"_index\":5,\"name\":{\"6\":{}},\"parent\":{\"59\":{},\"65\":{}}}],[\"data.enums\",{\"_index\":64,\"name\":{},\"parent\":{\"60\":{},\"62\":{}}}],[\"data.enums.role\",{\"_index\":68,\"name\":{},\"parent\":{\"63\":{},\"64\":{}}}],[\"data.enums.storagekey\",{\"_index\":66,\"name\":{},\"parent\":{\"61\":{}}}],[\"data.interfaces\",{\"_index\":72,\"name\":{},\"parent\":{\"66\":{}}}],[\"data.interfaces.user\",{\"_index\":74,\"name\":{},\"parent\":{\"67\":{},\"68\":{},\"69\":{},\"70\":{}}}],[\"default\",{\"_index\":1,\"name\":{\"1\":{},\"3\":{}},\"parent\":{}}],[\"editions\",{\"_index\":59,\"name\":{\"56\":{},\"70\":{}},\"parent\":{}}],[\"email\",{\"_index\":15,\"name\":{\"15\":{},\"20\":{},\"100\":{}},\"parent\":{}}],[\"enums\",{\"_index\":62,\"name\":{\"59\":{}},\"parent\":{}}],[\"errordiv\",{\"_index\":40,\"name\":{\"39\":{}},\"parent\":{}}],[\"errors\",{\"_index\":114,\"name\":{\"110\":{}},\"parent\":{}}],[\"footer\",{\"_index\":9,\"name\":{\"10\":{}},\"parent\":{}}],[\"forbiddenpage\",{\"_index\":115,\"name\":{\"111\":{}},\"parent\":{}}],[\"getadmins\",{\"_index\":82,\"name\":{\"76\":{}},\"parent\":{}}],[\"getcoaches\",{\"_index\":87,\"name\":{\"81\":{}},\"parent\":{}}],[\"getinvitelink\",{\"_index\":102,\"name\":{\"95\":{}},\"parent\":{}}],[\"getrequests\",{\"_index\":93,\"name\":{\"86\":{}},\"parent\":{}}],[\"getrequestsresponse\",{\"_index\":100,\"name\":{\"92\":{}},\"parent\":{}}],[\"gettoken\",{\"_index\":112,\"name\":{\"108\":{}},\"parent\":{}}],[\"getusers\",{\"_index\":104,\"name\":{\"96\":{}},\"parent\":{}}],[\"infotext\",{\"_index\":23,\"name\":{\"25\":{}},\"parent\":{}}],[\"interfaces\",{\"_index\":70,\"name\":{\"65\":{}},\"parent\":{}}],[\"inviteuser\",{\"_index\":35,\"name\":{\"35\":{},\"36\":{}},\"parent\":{}}],[\"inviteusercomponents\",{\"_index\":37,\"name\":{\"37\":{}},\"parent\":{}}],[\"isloggedin\",{\"_index\":52,\"name\":{\"50\":{}},\"parent\":{}}],[\"link\",{\"_index\":110,\"name\":{\"106\":{}},\"parent\":{}}],[\"linkdiv\",{\"_index\":41,\"name\":{\"40\":{}},\"parent\":{}}],[\"localstorage\",{\"_index\":111,\"name\":{\"107\":{}},\"parent\":{}}],[\"logincomponents\",{\"_index\":10,\"name\":{\"11\":{}},\"parent\":{}}],[\"loginpage\",{\"_index\":118,\"name\":{\"113\":{}},\"parent\":{}}],[\"mailto\",{\"_index\":108,\"name\":{\"104\":{},\"105\":{}},\"parent\":{}}],[\"name\",{\"_index\":21,\"name\":{\"21\":{},\"68\":{},\"99\":{}},\"parent\":{}}],[\"navbar\",{\"_index\":16,\"name\":{\"16\":{}},\"parent\":{}}],[\"notfoundpage\",{\"_index\":117,\"name\":{\"112\":{}},\"parent\":{}}],[\"osocletters\",{\"_index\":17,\"name\":{\"17\":{}},\"parent\":{}}],[\"password\",{\"_index\":14,\"name\":{\"14\":{},\"22\":{}},\"parent\":{}}],[\"pendingpage\",{\"_index\":119,\"name\":{\"114\":{}},\"parent\":{}}],[\"pendingrequests\",{\"_index\":42,\"name\":{\"41\":{},\"42\":{}},\"parent\":{}}],[\"pendingrequestscomponents\",{\"_index\":44,\"name\":{\"43\":{}},\"parent\":{}}],[\"privateroute\",{\"_index\":18,\"name\":{\"18\":{}},\"parent\":{}}],[\"projectspage\",{\"_index\":120,\"name\":{\"115\":{}},\"parent\":{}}],[\"registercomponents\",{\"_index\":19,\"name\":{\"19\":{}},\"parent\":{}}],[\"registerpage\",{\"_index\":121,\"name\":{\"116\":{}},\"parent\":{}}],[\"rejectrequest\",{\"_index\":96,\"name\":{\"88\":{}},\"parent\":{}}],[\"removeadmin\",{\"_index\":85,\"name\":{\"78\":{}},\"parent\":{}}],[\"removeadminandcoach\",{\"_index\":86,\"name\":{\"79\":{}},\"parent\":{}}],[\"removecoach\",{\"_index\":34,\"name\":{\"34\":{}},\"parent\":{}}],[\"removecoachfromalleditions\",{\"_index\":90,\"name\":{\"83\":{}},\"parent\":{}}],[\"removecoachfromedition\",{\"_index\":89,\"name\":{\"82\":{}},\"parent\":{}}],[\"request\",{\"_index\":97,\"name\":{\"89\":{}},\"parent\":{}}],[\"requestfilter\",{\"_index\":47,\"name\":{\"45\":{}},\"parent\":{}}],[\"requestid\",{\"_index\":98,\"name\":{\"90\":{}},\"parent\":{}}],[\"requestlist\",{\"_index\":49,\"name\":{\"47\":{}},\"parent\":{}}],[\"requestlistitem\",{\"_index\":48,\"name\":{\"46\":{}},\"parent\":{}}],[\"requests\",{\"_index\":92,\"name\":{\"85\":{},\"93\":{}},\"parent\":{}}],[\"requestsheader\",{\"_index\":50,\"name\":{\"48\":{}},\"parent\":{}}],[\"role\",{\"_index\":55,\"name\":{\"52\":{},\"62\":{}},\"parent\":{}}],[\"router\",{\"_index\":2,\"name\":{\"2\":{}},\"parent\":{\"3\":{}}}],[\"setbearertoken\",{\"_index\":78,\"name\":{\"73\":{}},\"parent\":{}}],[\"seteditions\",{\"_index\":60,\"name\":{\"57\":{}},\"parent\":{}}],[\"setisloggedin\",{\"_index\":54,\"name\":{\"51\":{}},\"parent\":{}}],[\"setrole\",{\"_index\":56,\"name\":{\"53\":{}},\"parent\":{}}],[\"settoken\",{\"_index\":58,\"name\":{\"55\":{},\"109\":{}},\"parent\":{}}],[\"socialbuttons\",{\"_index\":13,\"name\":{\"13\":{},\"24\":{}},\"parent\":{}}],[\"storagekey\",{\"_index\":63,\"name\":{\"60\":{}},\"parent\":{}}],[\"studentspage\",{\"_index\":122,\"name\":{\"117\":{}},\"parent\":{}}],[\"token\",{\"_index\":57,\"name\":{\"54\":{}},\"parent\":{}}],[\"user\",{\"_index\":71,\"name\":{\"66\":{},\"91\":{},\"97\":{}},\"parent\":{}}],[\"userid\",{\"_index\":73,\"name\":{\"67\":{},\"98\":{}},\"parent\":{}}],[\"users\",{\"_index\":79,\"name\":{\"74\":{},\"94\":{},\"103\":{}},\"parent\":{}}],[\"userscomponents\",{\"_index\":25,\"name\":{\"27\":{}},\"parent\":{}}],[\"userslist\",{\"_index\":106,\"name\":{\"102\":{}},\"parent\":{}}],[\"userspage\",{\"_index\":123,\"name\":{\"118\":{}},\"parent\":{}}],[\"utils\",{\"_index\":6,\"name\":{\"7\":{}},\"parent\":{\"71\":{},\"107\":{}}}],[\"utils.api\",{\"_index\":77,\"name\":{},\"parent\":{\"72\":{},\"73\":{},\"74\":{}}}],[\"utils.api.users\",{\"_index\":81,\"name\":{},\"parent\":{\"75\":{},\"80\":{},\"85\":{},\"94\":{}}}],[\"utils.api.users.admins\",{\"_index\":83,\"name\":{},\"parent\":{\"76\":{},\"77\":{},\"78\":{},\"79\":{}}}],[\"utils.api.users.coaches\",{\"_index\":88,\"name\":{},\"parent\":{\"81\":{},\"82\":{},\"83\":{},\"84\":{}}}],[\"utils.api.users.requests\",{\"_index\":94,\"name\":{},\"parent\":{\"86\":{},\"87\":{},\"88\":{},\"89\":{},\"92\":{}}}],[\"utils.api.users.requests.getrequestsresponse\",{\"_index\":101,\"name\":{},\"parent\":{\"93\":{}}}],[\"utils.api.users.requests.request\",{\"_index\":99,\"name\":{},\"parent\":{\"90\":{},\"91\":{}}}],[\"utils.api.users.users\",{\"_index\":103,\"name\":{},\"parent\":{\"95\":{},\"96\":{},\"97\":{},\"102\":{},\"104\":{}}}],[\"utils.api.users.users.mailto\",{\"_index\":109,\"name\":{},\"parent\":{\"105\":{},\"106\":{}}}],[\"utils.api.users.users.user\",{\"_index\":105,\"name\":{},\"parent\":{\"98\":{},\"99\":{},\"100\":{},\"101\":{}}}],[\"utils.api.users.users.userslist\",{\"_index\":107,\"name\":{},\"parent\":{\"103\":{}}}],[\"utils.localstorage\",{\"_index\":113,\"name\":{},\"parent\":{\"108\":{},\"109\":{}}}],[\"validateregistrationurl\",{\"_index\":76,\"name\":{\"72\":{}},\"parent\":{}}],[\"verifyingtokenpage\",{\"_index\":124,\"name\":{\"119\":{}},\"parent\":{}}],[\"views\",{\"_index\":7,\"name\":{\"8\":{}},\"parent\":{\"110\":{},\"113\":{},\"114\":{},\"115\":{},\"116\":{},\"117\":{},\"118\":{},\"119\":{}}}],[\"views.errors\",{\"_index\":116,\"name\":{},\"parent\":{\"111\":{},\"112\":{}}}],[\"welcometext\",{\"_index\":11,\"name\":{\"12\":{}},\"parent\":{}}]],\"pipeline\":[]}}"); \ No newline at end of file +window.searchData = JSON.parse("{\"kinds\":{\"2\":\"Module\",\"4\":\"Namespace\",\"8\":\"Enumeration\",\"16\":\"Enumeration member\",\"64\":\"Function\",\"256\":\"Interface\",\"1024\":\"Property\",\"2048\":\"Method\"},\"rows\":[{\"id\":0,\"kind\":2,\"name\":\"App\",\"url\":\"modules/App.html\",\"classes\":\"tsd-kind-module\"},{\"id\":1,\"kind\":64,\"name\":\"default\",\"url\":\"modules/App.html#default\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"App\"},{\"id\":2,\"kind\":2,\"name\":\"Router\",\"url\":\"modules/Router.html\",\"classes\":\"tsd-kind-module\"},{\"id\":3,\"kind\":64,\"name\":\"default\",\"url\":\"modules/Router.html#default\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"Router\"},{\"id\":4,\"kind\":2,\"name\":\"components\",\"url\":\"modules/components.html\",\"classes\":\"tsd-kind-module\"},{\"id\":5,\"kind\":2,\"name\":\"contexts\",\"url\":\"modules/contexts.html\",\"classes\":\"tsd-kind-module\"},{\"id\":6,\"kind\":2,\"name\":\"data\",\"url\":\"modules/data.html\",\"classes\":\"tsd-kind-module\"},{\"id\":7,\"kind\":2,\"name\":\"utils\",\"url\":\"modules/utils.html\",\"classes\":\"tsd-kind-module\"},{\"id\":8,\"kind\":2,\"name\":\"views\",\"url\":\"modules/views.html\",\"classes\":\"tsd-kind-module\"},{\"id\":9,\"kind\":64,\"name\":\"AdminRoute\",\"url\":\"modules/components.html#AdminRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":10,\"kind\":64,\"name\":\"Footer\",\"url\":\"modules/components.html#Footer\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":11,\"kind\":4,\"name\":\"LoginComponents\",\"url\":\"modules/components.LoginComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":12,\"kind\":64,\"name\":\"WelcomeText\",\"url\":\"modules/components.LoginComponents.html#WelcomeText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":13,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/components.LoginComponents.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":14,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/components.LoginComponents.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":15,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/components.LoginComponents.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":16,\"kind\":64,\"name\":\"NavBar\",\"url\":\"modules/components.html#NavBar\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":17,\"kind\":64,\"name\":\"OSOCLetters\",\"url\":\"modules/components.html#OSOCLetters\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":18,\"kind\":64,\"name\":\"PrivateRoute\",\"url\":\"modules/components.html#PrivateRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":19,\"kind\":4,\"name\":\"RegisterComponents\",\"url\":\"modules/components.RegisterComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":20,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/components.RegisterComponents.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":21,\"kind\":64,\"name\":\"Name\",\"url\":\"modules/components.RegisterComponents.html#Name\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":22,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/components.RegisterComponents.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":23,\"kind\":64,\"name\":\"ConfirmPassword\",\"url\":\"modules/components.RegisterComponents.html#ConfirmPassword\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":24,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/components.RegisterComponents.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":25,\"kind\":64,\"name\":\"InfoText\",\"url\":\"modules/components.RegisterComponents.html#InfoText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":26,\"kind\":64,\"name\":\"BadInviteLink\",\"url\":\"modules/components.RegisterComponents.html#BadInviteLink\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":27,\"kind\":4,\"name\":\"UsersComponents\",\"url\":\"modules/components.UsersComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":28,\"kind\":4,\"name\":\"Coaches\",\"url\":\"modules/components.UsersComponents.Coaches.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents\"},{\"id\":29,\"kind\":64,\"name\":\"Coaches\",\"url\":\"modules/components.UsersComponents.Coaches.html#Coaches\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches\"},{\"id\":30,\"kind\":4,\"name\":\"CoachesComponents\",\"url\":\"modules/components.UsersComponents.Coaches.CoachesComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches\"},{\"id\":31,\"kind\":64,\"name\":\"AddCoach\",\"url\":\"modules/components.UsersComponents.Coaches.CoachesComponents.html#AddCoach\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches.CoachesComponents\"},{\"id\":32,\"kind\":64,\"name\":\"CoachListItem\",\"url\":\"modules/components.UsersComponents.Coaches.CoachesComponents.html#CoachListItem\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches.CoachesComponents\"},{\"id\":33,\"kind\":64,\"name\":\"CoachList\",\"url\":\"modules/components.UsersComponents.Coaches.CoachesComponents.html#CoachList\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches.CoachesComponents\"},{\"id\":34,\"kind\":64,\"name\":\"RemoveCoach\",\"url\":\"modules/components.UsersComponents.Coaches.CoachesComponents.html#RemoveCoach\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.Coaches.CoachesComponents\"},{\"id\":35,\"kind\":4,\"name\":\"InviteUser\",\"url\":\"modules/components.UsersComponents.InviteUser.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents\"},{\"id\":36,\"kind\":64,\"name\":\"InviteUser\",\"url\":\"modules/components.UsersComponents.InviteUser.html#InviteUser\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.InviteUser\"},{\"id\":37,\"kind\":4,\"name\":\"InviteUserComponents\",\"url\":\"modules/components.UsersComponents.InviteUser.InviteUserComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.InviteUser\"},{\"id\":38,\"kind\":64,\"name\":\"ButtonsDiv\",\"url\":\"modules/components.UsersComponents.InviteUser.InviteUserComponents.html#ButtonsDiv\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.InviteUser.InviteUserComponents\"},{\"id\":39,\"kind\":64,\"name\":\"ErrorDiv\",\"url\":\"modules/components.UsersComponents.InviteUser.InviteUserComponents.html#ErrorDiv\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.InviteUser.InviteUserComponents\"},{\"id\":40,\"kind\":4,\"name\":\"PendingRequests\",\"url\":\"modules/components.UsersComponents.PendingRequests.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents\"},{\"id\":41,\"kind\":64,\"name\":\"PendingRequests\",\"url\":\"modules/components.UsersComponents.PendingRequests.html#PendingRequests\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests\"},{\"id\":42,\"kind\":4,\"name\":\"PendingRequestsComponents\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests\"},{\"id\":43,\"kind\":64,\"name\":\"AcceptReject\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html#AcceptReject\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests.PendingRequestsComponents\"},{\"id\":44,\"kind\":64,\"name\":\"RequestFilter\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html#RequestFilter\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests.PendingRequestsComponents\"},{\"id\":45,\"kind\":64,\"name\":\"RequestListItem\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html#RequestListItem\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests.PendingRequestsComponents\"},{\"id\":46,\"kind\":64,\"name\":\"RequestList\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html#RequestList\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests.PendingRequestsComponents\"},{\"id\":47,\"kind\":64,\"name\":\"RequestsHeader\",\"url\":\"modules/components.UsersComponents.PendingRequests.PendingRequestsComponents.html#RequestsHeader\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.UsersComponents.PendingRequests.PendingRequestsComponents\"},{\"id\":48,\"kind\":256,\"name\":\"AuthContextState\",\"url\":\"interfaces/contexts.AuthContextState.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-module\",\"parent\":\"contexts\"},{\"id\":49,\"kind\":1024,\"name\":\"isLoggedIn\",\"url\":\"interfaces/contexts.AuthContextState.html#isLoggedIn\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":50,\"kind\":2048,\"name\":\"setIsLoggedIn\",\"url\":\"interfaces/contexts.AuthContextState.html#setIsLoggedIn\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":51,\"kind\":1024,\"name\":\"role\",\"url\":\"interfaces/contexts.AuthContextState.html#role\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":52,\"kind\":2048,\"name\":\"setRole\",\"url\":\"interfaces/contexts.AuthContextState.html#setRole\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":53,\"kind\":1024,\"name\":\"token\",\"url\":\"interfaces/contexts.AuthContextState.html#token\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":54,\"kind\":2048,\"name\":\"setToken\",\"url\":\"interfaces/contexts.AuthContextState.html#setToken\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":55,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/contexts.AuthContextState.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":56,\"kind\":2048,\"name\":\"setEditions\",\"url\":\"interfaces/contexts.AuthContextState.html#setEditions\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":57,\"kind\":64,\"name\":\"AuthProvider\",\"url\":\"modules/contexts.html#AuthProvider\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"contexts\"},{\"id\":58,\"kind\":4,\"name\":\"Enums\",\"url\":\"modules/data.Enums.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"data\"},{\"id\":59,\"kind\":8,\"name\":\"StorageKey\",\"url\":\"enums/data.Enums.StorageKey.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"data.Enums\"},{\"id\":60,\"kind\":16,\"name\":\"BEARER_TOKEN\",\"url\":\"enums/data.Enums.StorageKey.html#BEARER_TOKEN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.StorageKey\"},{\"id\":61,\"kind\":8,\"name\":\"Role\",\"url\":\"enums/data.Enums.Role.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"data.Enums\"},{\"id\":62,\"kind\":16,\"name\":\"ADMIN\",\"url\":\"enums/data.Enums.Role.html#ADMIN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.Role\"},{\"id\":63,\"kind\":16,\"name\":\"COACH\",\"url\":\"enums/data.Enums.Role.html#COACH\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.Role\"},{\"id\":64,\"kind\":4,\"name\":\"Interfaces\",\"url\":\"modules/data.Interfaces.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"data\"},{\"id\":65,\"kind\":256,\"name\":\"User\",\"url\":\"interfaces/data.Interfaces.User.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"data.Interfaces\"},{\"id\":66,\"kind\":1024,\"name\":\"userId\",\"url\":\"interfaces/data.Interfaces.User.html#userId\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":67,\"kind\":1024,\"name\":\"name\",\"url\":\"interfaces/data.Interfaces.User.html#name\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":68,\"kind\":1024,\"name\":\"admin\",\"url\":\"interfaces/data.Interfaces.User.html#admin\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":69,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/data.Interfaces.User.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":70,\"kind\":4,\"name\":\"Api\",\"url\":\"modules/utils.Api.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"utils\"},{\"id\":71,\"kind\":64,\"name\":\"validateRegistrationUrl\",\"url\":\"modules/utils.Api.html#validateRegistrationUrl\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api\"},{\"id\":72,\"kind\":64,\"name\":\"setBearerToken\",\"url\":\"modules/utils.Api.html#setBearerToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api\"},{\"id\":73,\"kind\":4,\"name\":\"Users\",\"url\":\"modules/utils.Api.Users.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"utils.Api\"},{\"id\":74,\"kind\":4,\"name\":\"Admins\",\"url\":\"modules/utils.Api.Users.Admins.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users\"},{\"id\":75,\"kind\":64,\"name\":\"getAdmins\",\"url\":\"modules/utils.Api.Users.Admins.html#getAdmins\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Admins\"},{\"id\":76,\"kind\":64,\"name\":\"addAdmin\",\"url\":\"modules/utils.Api.Users.Admins.html#addAdmin\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Admins\"},{\"id\":77,\"kind\":64,\"name\":\"removeAdmin\",\"url\":\"modules/utils.Api.Users.Admins.html#removeAdmin\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Admins\"},{\"id\":78,\"kind\":64,\"name\":\"removeAdminAndCoach\",\"url\":\"modules/utils.Api.Users.Admins.html#removeAdminAndCoach\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Admins\"},{\"id\":79,\"kind\":4,\"name\":\"Coaches\",\"url\":\"modules/utils.Api.Users.Coaches.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users\"},{\"id\":80,\"kind\":64,\"name\":\"getCoaches\",\"url\":\"modules/utils.Api.Users.Coaches.html#getCoaches\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Coaches\"},{\"id\":81,\"kind\":64,\"name\":\"removeCoachFromEdition\",\"url\":\"modules/utils.Api.Users.Coaches.html#removeCoachFromEdition\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Coaches\"},{\"id\":82,\"kind\":64,\"name\":\"removeCoachFromAllEditions\",\"url\":\"modules/utils.Api.Users.Coaches.html#removeCoachFromAllEditions\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Coaches\"},{\"id\":83,\"kind\":64,\"name\":\"addCoachToEdition\",\"url\":\"modules/utils.Api.Users.Coaches.html#addCoachToEdition\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Coaches\"},{\"id\":84,\"kind\":4,\"name\":\"Requests\",\"url\":\"modules/utils.Api.Users.Requests.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users\"},{\"id\":85,\"kind\":64,\"name\":\"getRequests\",\"url\":\"modules/utils.Api.Users.Requests.html#getRequests\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Requests\"},{\"id\":86,\"kind\":64,\"name\":\"acceptRequest\",\"url\":\"modules/utils.Api.Users.Requests.html#acceptRequest\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Requests\"},{\"id\":87,\"kind\":64,\"name\":\"rejectRequest\",\"url\":\"modules/utils.Api.Users.Requests.html#rejectRequest\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Requests\"},{\"id\":88,\"kind\":256,\"name\":\"Request\",\"url\":\"interfaces/utils.Api.Users.Requests.Request.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Requests\"},{\"id\":89,\"kind\":1024,\"name\":\"requestId\",\"url\":\"interfaces/utils.Api.Users.Requests.Request.html#requestId\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Requests.Request\"},{\"id\":90,\"kind\":1024,\"name\":\"user\",\"url\":\"interfaces/utils.Api.Users.Requests.Request.html#user\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Requests.Request\"},{\"id\":91,\"kind\":256,\"name\":\"GetRequestsResponse\",\"url\":\"interfaces/utils.Api.Users.Requests.GetRequestsResponse.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Requests\"},{\"id\":92,\"kind\":1024,\"name\":\"requests\",\"url\":\"interfaces/utils.Api.Users.Requests.GetRequestsResponse.html#requests\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Requests.GetRequestsResponse\"},{\"id\":93,\"kind\":4,\"name\":\"Users\",\"url\":\"modules/utils.Api.Users.Users.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users\"},{\"id\":94,\"kind\":64,\"name\":\"getInviteLink\",\"url\":\"modules/utils.Api.Users.Users.html#getInviteLink\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Users\"},{\"id\":95,\"kind\":64,\"name\":\"getUsers\",\"url\":\"modules/utils.Api.Users.Users.html#getUsers\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Users\"},{\"id\":96,\"kind\":256,\"name\":\"User\",\"url\":\"interfaces/utils.Api.Users.Users.User.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Users\"},{\"id\":97,\"kind\":1024,\"name\":\"userId\",\"url\":\"interfaces/utils.Api.Users.Users.User.html#userId\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.User\"},{\"id\":98,\"kind\":1024,\"name\":\"name\",\"url\":\"interfaces/utils.Api.Users.Users.User.html#name\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.User\"},{\"id\":99,\"kind\":1024,\"name\":\"email\",\"url\":\"interfaces/utils.Api.Users.Users.User.html#email\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.User\"},{\"id\":100,\"kind\":1024,\"name\":\"admin\",\"url\":\"interfaces/utils.Api.Users.Users.User.html#admin\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.User\"},{\"id\":101,\"kind\":256,\"name\":\"UsersList\",\"url\":\"interfaces/utils.Api.Users.Users.UsersList.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Users\"},{\"id\":102,\"kind\":1024,\"name\":\"users\",\"url\":\"interfaces/utils.Api.Users.Users.UsersList.html#users\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.UsersList\"},{\"id\":103,\"kind\":256,\"name\":\"MailTo\",\"url\":\"interfaces/utils.Api.Users.Users.MailTo.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"utils.Api.Users.Users\"},{\"id\":104,\"kind\":1024,\"name\":\"mailTo\",\"url\":\"interfaces/utils.Api.Users.Users.MailTo.html#mailTo\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.MailTo\"},{\"id\":105,\"kind\":1024,\"name\":\"link\",\"url\":\"interfaces/utils.Api.Users.Users.MailTo.html#link\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"utils.Api.Users.Users.MailTo\"},{\"id\":106,\"kind\":4,\"name\":\"LocalStorage\",\"url\":\"modules/utils.LocalStorage.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"utils\"},{\"id\":107,\"kind\":64,\"name\":\"getToken\",\"url\":\"modules/utils.LocalStorage.html#getToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.LocalStorage\"},{\"id\":108,\"kind\":64,\"name\":\"setToken\",\"url\":\"modules/utils.LocalStorage.html#setToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.LocalStorage\"},{\"id\":109,\"kind\":4,\"name\":\"Errors\",\"url\":\"modules/views.Errors.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":110,\"kind\":64,\"name\":\"ForbiddenPage\",\"url\":\"modules/views.Errors.html#ForbiddenPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"views.Errors\"},{\"id\":111,\"kind\":64,\"name\":\"NotFoundPage\",\"url\":\"modules/views.Errors.html#NotFoundPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"views.Errors\"},{\"id\":112,\"kind\":64,\"name\":\"LoginPage\",\"url\":\"modules/views.html#LoginPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":113,\"kind\":64,\"name\":\"PendingPage\",\"url\":\"modules/views.html#PendingPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":114,\"kind\":64,\"name\":\"ProjectsPage\",\"url\":\"modules/views.html#ProjectsPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":115,\"kind\":64,\"name\":\"RegisterPage\",\"url\":\"modules/views.html#RegisterPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":116,\"kind\":64,\"name\":\"StudentsPage\",\"url\":\"modules/views.html#StudentsPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":117,\"kind\":64,\"name\":\"UsersPage\",\"url\":\"modules/views.html#UsersPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":118,\"kind\":64,\"name\":\"VerifyingTokenPage\",\"url\":\"modules/views.html#VerifyingTokenPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"parent\"],\"fieldVectors\":[[\"name/0\",[0,38.712]],[\"parent/0\",[]],[\"name/1\",[1,38.712]],[\"parent/1\",[0,3.775]],[\"name/2\",[2,38.712]],[\"parent/2\",[]],[\"name/3\",[1,38.712]],[\"parent/3\",[2,3.775]],[\"name/4\",[3,25.362]],[\"parent/4\",[]],[\"name/5\",[4,35.347]],[\"parent/5\",[]],[\"name/6\",[5,35.347]],[\"parent/6\",[]],[\"name/7\",[6,35.347]],[\"parent/7\",[]],[\"name/8\",[7,25.362]],[\"parent/8\",[]],[\"name/9\",[8,43.82]],[\"parent/9\",[3,2.473]],[\"name/10\",[9,43.82]],[\"parent/10\",[3,2.473]],[\"name/11\",[10,43.82]],[\"parent/11\",[3,2.473]],[\"name/12\",[11,43.82]],[\"parent/12\",[12,3.202]],[\"name/13\",[13,38.712]],[\"parent/13\",[12,3.202]],[\"name/14\",[14,38.712]],[\"parent/14\",[12,3.202]],[\"name/15\",[15,35.347]],[\"parent/15\",[12,3.202]],[\"name/16\",[16,43.82]],[\"parent/16\",[3,2.473]],[\"name/17\",[17,43.82]],[\"parent/17\",[3,2.473]],[\"name/18\",[18,43.82]],[\"parent/18\",[3,2.473]],[\"name/19\",[19,43.82]],[\"parent/19\",[3,2.473]],[\"name/20\",[15,35.347]],[\"parent/20\",[20,2.703]],[\"name/21\",[21,35.347]],[\"parent/21\",[20,2.703]],[\"name/22\",[14,38.712]],[\"parent/22\",[20,2.703]],[\"name/23\",[22,43.82]],[\"parent/23\",[20,2.703]],[\"name/24\",[13,38.712]],[\"parent/24\",[20,2.703]],[\"name/25\",[23,43.82]],[\"parent/25\",[20,2.703]],[\"name/26\",[24,43.82]],[\"parent/26\",[20,2.703]],[\"name/27\",[25,43.82]],[\"parent/27\",[3,2.473]],[\"name/28\",[26,35.347]],[\"parent/28\",[27,3.447]],[\"name/29\",[26,35.347]],[\"parent/29\",[28,3.775]],[\"name/30\",[29,43.82]],[\"parent/30\",[28,3.775]],[\"name/31\",[30,43.82]],[\"parent/31\",[31,3.202]],[\"name/32\",[32,43.82]],[\"parent/32\",[31,3.202]],[\"name/33\",[33,43.82]],[\"parent/33\",[31,3.202]],[\"name/34\",[34,43.82]],[\"parent/34\",[31,3.202]],[\"name/35\",[35,38.712]],[\"parent/35\",[27,3.447]],[\"name/36\",[35,38.712]],[\"parent/36\",[36,3.775]],[\"name/37\",[37,43.82]],[\"parent/37\",[36,3.775]],[\"name/38\",[38,43.82]],[\"parent/38\",[39,3.775]],[\"name/39\",[40,43.82]],[\"parent/39\",[39,3.775]],[\"name/40\",[41,38.712]],[\"parent/40\",[27,3.447]],[\"name/41\",[41,38.712]],[\"parent/41\",[42,3.775]],[\"name/42\",[43,43.82]],[\"parent/42\",[42,3.775]],[\"name/43\",[44,43.82]],[\"parent/43\",[45,3.006]],[\"name/44\",[46,43.82]],[\"parent/44\",[45,3.006]],[\"name/45\",[47,43.82]],[\"parent/45\",[45,3.006]],[\"name/46\",[48,43.82]],[\"parent/46\",[45,3.006]],[\"name/47\",[49,43.82]],[\"parent/47\",[45,3.006]],[\"name/48\",[50,43.82]],[\"parent/48\",[4,3.447]],[\"name/49\",[51,43.82]],[\"parent/49\",[52,2.581]],[\"name/50\",[53,43.82]],[\"parent/50\",[52,2.581]],[\"name/51\",[54,38.712]],[\"parent/51\",[52,2.581]],[\"name/52\",[55,43.82]],[\"parent/52\",[52,2.581]],[\"name/53\",[56,43.82]],[\"parent/53\",[52,2.581]],[\"name/54\",[57,38.712]],[\"parent/54\",[52,2.581]],[\"name/55\",[58,38.712]],[\"parent/55\",[52,2.581]],[\"name/56\",[59,43.82]],[\"parent/56\",[52,2.581]],[\"name/57\",[60,43.82]],[\"parent/57\",[4,3.447]],[\"name/58\",[61,43.82]],[\"parent/58\",[5,3.447]],[\"name/59\",[62,43.82]],[\"parent/59\",[63,3.775]],[\"name/60\",[64,43.82]],[\"parent/60\",[65,4.273]],[\"name/61\",[54,38.712]],[\"parent/61\",[63,3.775]],[\"name/62\",[66,35.347]],[\"parent/62\",[67,3.775]],[\"name/63\",[68,43.82]],[\"parent/63\",[67,3.775]],[\"name/64\",[69,43.82]],[\"parent/64\",[5,3.447]],[\"name/65\",[70,35.347]],[\"parent/65\",[71,4.273]],[\"name/66\",[72,38.712]],[\"parent/66\",[73,3.202]],[\"name/67\",[21,35.347]],[\"parent/67\",[73,3.202]],[\"name/68\",[66,35.347]],[\"parent/68\",[73,3.202]],[\"name/69\",[58,38.712]],[\"parent/69\",[73,3.202]],[\"name/70\",[74,43.82]],[\"parent/70\",[6,3.447]],[\"name/71\",[75,43.82]],[\"parent/71\",[76,3.447]],[\"name/72\",[77,43.82]],[\"parent/72\",[76,3.447]],[\"name/73\",[78,35.347]],[\"parent/73\",[76,3.447]],[\"name/74\",[79,43.82]],[\"parent/74\",[80,3.202]],[\"name/75\",[81,43.82]],[\"parent/75\",[82,3.202]],[\"name/76\",[83,43.82]],[\"parent/76\",[82,3.202]],[\"name/77\",[84,43.82]],[\"parent/77\",[82,3.202]],[\"name/78\",[85,43.82]],[\"parent/78\",[82,3.202]],[\"name/79\",[26,35.347]],[\"parent/79\",[80,3.202]],[\"name/80\",[86,43.82]],[\"parent/80\",[87,3.202]],[\"name/81\",[88,43.82]],[\"parent/81\",[87,3.202]],[\"name/82\",[89,43.82]],[\"parent/82\",[87,3.202]],[\"name/83\",[90,43.82]],[\"parent/83\",[87,3.202]],[\"name/84\",[91,38.712]],[\"parent/84\",[80,3.202]],[\"name/85\",[92,43.82]],[\"parent/85\",[93,3.006]],[\"name/86\",[94,43.82]],[\"parent/86\",[93,3.006]],[\"name/87\",[95,43.82]],[\"parent/87\",[93,3.006]],[\"name/88\",[96,43.82]],[\"parent/88\",[93,3.006]],[\"name/89\",[97,43.82]],[\"parent/89\",[98,3.775]],[\"name/90\",[70,35.347]],[\"parent/90\",[98,3.775]],[\"name/91\",[99,43.82]],[\"parent/91\",[93,3.006]],[\"name/92\",[91,38.712]],[\"parent/92\",[100,4.273]],[\"name/93\",[78,35.347]],[\"parent/93\",[80,3.202]],[\"name/94\",[101,43.82]],[\"parent/94\",[102,3.006]],[\"name/95\",[103,43.82]],[\"parent/95\",[102,3.006]],[\"name/96\",[70,35.347]],[\"parent/96\",[102,3.006]],[\"name/97\",[72,38.712]],[\"parent/97\",[104,3.202]],[\"name/98\",[21,35.347]],[\"parent/98\",[104,3.202]],[\"name/99\",[15,35.347]],[\"parent/99\",[104,3.202]],[\"name/100\",[66,35.347]],[\"parent/100\",[104,3.202]],[\"name/101\",[105,43.82]],[\"parent/101\",[102,3.006]],[\"name/102\",[78,35.347]],[\"parent/102\",[106,4.273]],[\"name/103\",[107,38.712]],[\"parent/103\",[102,3.006]],[\"name/104\",[107,38.712]],[\"parent/104\",[108,3.775]],[\"name/105\",[109,43.82]],[\"parent/105\",[108,3.775]],[\"name/106\",[110,43.82]],[\"parent/106\",[6,3.447]],[\"name/107\",[111,43.82]],[\"parent/107\",[112,3.775]],[\"name/108\",[57,38.712]],[\"parent/108\",[112,3.775]],[\"name/109\",[113,43.82]],[\"parent/109\",[7,2.473]],[\"name/110\",[114,43.82]],[\"parent/110\",[115,3.775]],[\"name/111\",[116,43.82]],[\"parent/111\",[115,3.775]],[\"name/112\",[117,43.82]],[\"parent/112\",[7,2.473]],[\"name/113\",[118,43.82]],[\"parent/113\",[7,2.473]],[\"name/114\",[119,43.82]],[\"parent/114\",[7,2.473]],[\"name/115\",[120,43.82]],[\"parent/115\",[7,2.473]],[\"name/116\",[121,43.82]],[\"parent/116\",[7,2.473]],[\"name/117\",[122,43.82]],[\"parent/117\",[7,2.473]],[\"name/118\",[123,43.82]],[\"parent/118\",[7,2.473]]],\"invertedIndex\":[[\"acceptreject\",{\"_index\":44,\"name\":{\"43\":{}},\"parent\":{}}],[\"acceptrequest\",{\"_index\":94,\"name\":{\"86\":{}},\"parent\":{}}],[\"addadmin\",{\"_index\":83,\"name\":{\"76\":{}},\"parent\":{}}],[\"addcoach\",{\"_index\":30,\"name\":{\"31\":{}},\"parent\":{}}],[\"addcoachtoedition\",{\"_index\":90,\"name\":{\"83\":{}},\"parent\":{}}],[\"admin\",{\"_index\":66,\"name\":{\"62\":{},\"68\":{},\"100\":{}},\"parent\":{}}],[\"adminroute\",{\"_index\":8,\"name\":{\"9\":{}},\"parent\":{}}],[\"admins\",{\"_index\":79,\"name\":{\"74\":{}},\"parent\":{}}],[\"api\",{\"_index\":74,\"name\":{\"70\":{}},\"parent\":{}}],[\"app\",{\"_index\":0,\"name\":{\"0\":{}},\"parent\":{\"1\":{}}}],[\"authcontextstate\",{\"_index\":50,\"name\":{\"48\":{}},\"parent\":{}}],[\"authprovider\",{\"_index\":60,\"name\":{\"57\":{}},\"parent\":{}}],[\"badinvitelink\",{\"_index\":24,\"name\":{\"26\":{}},\"parent\":{}}],[\"bearer_token\",{\"_index\":64,\"name\":{\"60\":{}},\"parent\":{}}],[\"buttonsdiv\",{\"_index\":38,\"name\":{\"38\":{}},\"parent\":{}}],[\"coach\",{\"_index\":68,\"name\":{\"63\":{}},\"parent\":{}}],[\"coaches\",{\"_index\":26,\"name\":{\"28\":{},\"29\":{},\"79\":{}},\"parent\":{}}],[\"coachescomponents\",{\"_index\":29,\"name\":{\"30\":{}},\"parent\":{}}],[\"coachlist\",{\"_index\":33,\"name\":{\"33\":{}},\"parent\":{}}],[\"coachlistitem\",{\"_index\":32,\"name\":{\"32\":{}},\"parent\":{}}],[\"components\",{\"_index\":3,\"name\":{\"4\":{}},\"parent\":{\"9\":{},\"10\":{},\"11\":{},\"16\":{},\"17\":{},\"18\":{},\"19\":{},\"27\":{}}}],[\"components.logincomponents\",{\"_index\":12,\"name\":{},\"parent\":{\"12\":{},\"13\":{},\"14\":{},\"15\":{}}}],[\"components.registercomponents\",{\"_index\":20,\"name\":{},\"parent\":{\"20\":{},\"21\":{},\"22\":{},\"23\":{},\"24\":{},\"25\":{},\"26\":{}}}],[\"components.userscomponents\",{\"_index\":27,\"name\":{},\"parent\":{\"28\":{},\"35\":{},\"40\":{}}}],[\"components.userscomponents.coaches\",{\"_index\":28,\"name\":{},\"parent\":{\"29\":{},\"30\":{}}}],[\"components.userscomponents.coaches.coachescomponents\",{\"_index\":31,\"name\":{},\"parent\":{\"31\":{},\"32\":{},\"33\":{},\"34\":{}}}],[\"components.userscomponents.inviteuser\",{\"_index\":36,\"name\":{},\"parent\":{\"36\":{},\"37\":{}}}],[\"components.userscomponents.inviteuser.inviteusercomponents\",{\"_index\":39,\"name\":{},\"parent\":{\"38\":{},\"39\":{}}}],[\"components.userscomponents.pendingrequests\",{\"_index\":42,\"name\":{},\"parent\":{\"41\":{},\"42\":{}}}],[\"components.userscomponents.pendingrequests.pendingrequestscomponents\",{\"_index\":45,\"name\":{},\"parent\":{\"43\":{},\"44\":{},\"45\":{},\"46\":{},\"47\":{}}}],[\"confirmpassword\",{\"_index\":22,\"name\":{\"23\":{}},\"parent\":{}}],[\"contexts\",{\"_index\":4,\"name\":{\"5\":{}},\"parent\":{\"48\":{},\"57\":{}}}],[\"contexts.authcontextstate\",{\"_index\":52,\"name\":{},\"parent\":{\"49\":{},\"50\":{},\"51\":{},\"52\":{},\"53\":{},\"54\":{},\"55\":{},\"56\":{}}}],[\"data\",{\"_index\":5,\"name\":{\"6\":{}},\"parent\":{\"58\":{},\"64\":{}}}],[\"data.enums\",{\"_index\":63,\"name\":{},\"parent\":{\"59\":{},\"61\":{}}}],[\"data.enums.role\",{\"_index\":67,\"name\":{},\"parent\":{\"62\":{},\"63\":{}}}],[\"data.enums.storagekey\",{\"_index\":65,\"name\":{},\"parent\":{\"60\":{}}}],[\"data.interfaces\",{\"_index\":71,\"name\":{},\"parent\":{\"65\":{}}}],[\"data.interfaces.user\",{\"_index\":73,\"name\":{},\"parent\":{\"66\":{},\"67\":{},\"68\":{},\"69\":{}}}],[\"default\",{\"_index\":1,\"name\":{\"1\":{},\"3\":{}},\"parent\":{}}],[\"editions\",{\"_index\":58,\"name\":{\"55\":{},\"69\":{}},\"parent\":{}}],[\"email\",{\"_index\":15,\"name\":{\"15\":{},\"20\":{},\"99\":{}},\"parent\":{}}],[\"enums\",{\"_index\":61,\"name\":{\"58\":{}},\"parent\":{}}],[\"errordiv\",{\"_index\":40,\"name\":{\"39\":{}},\"parent\":{}}],[\"errors\",{\"_index\":113,\"name\":{\"109\":{}},\"parent\":{}}],[\"footer\",{\"_index\":9,\"name\":{\"10\":{}},\"parent\":{}}],[\"forbiddenpage\",{\"_index\":114,\"name\":{\"110\":{}},\"parent\":{}}],[\"getadmins\",{\"_index\":81,\"name\":{\"75\":{}},\"parent\":{}}],[\"getcoaches\",{\"_index\":86,\"name\":{\"80\":{}},\"parent\":{}}],[\"getinvitelink\",{\"_index\":101,\"name\":{\"94\":{}},\"parent\":{}}],[\"getrequests\",{\"_index\":92,\"name\":{\"85\":{}},\"parent\":{}}],[\"getrequestsresponse\",{\"_index\":99,\"name\":{\"91\":{}},\"parent\":{}}],[\"gettoken\",{\"_index\":111,\"name\":{\"107\":{}},\"parent\":{}}],[\"getusers\",{\"_index\":103,\"name\":{\"95\":{}},\"parent\":{}}],[\"infotext\",{\"_index\":23,\"name\":{\"25\":{}},\"parent\":{}}],[\"interfaces\",{\"_index\":69,\"name\":{\"64\":{}},\"parent\":{}}],[\"inviteuser\",{\"_index\":35,\"name\":{\"35\":{},\"36\":{}},\"parent\":{}}],[\"inviteusercomponents\",{\"_index\":37,\"name\":{\"37\":{}},\"parent\":{}}],[\"isloggedin\",{\"_index\":51,\"name\":{\"49\":{}},\"parent\":{}}],[\"link\",{\"_index\":109,\"name\":{\"105\":{}},\"parent\":{}}],[\"localstorage\",{\"_index\":110,\"name\":{\"106\":{}},\"parent\":{}}],[\"logincomponents\",{\"_index\":10,\"name\":{\"11\":{}},\"parent\":{}}],[\"loginpage\",{\"_index\":117,\"name\":{\"112\":{}},\"parent\":{}}],[\"mailto\",{\"_index\":107,\"name\":{\"103\":{},\"104\":{}},\"parent\":{}}],[\"name\",{\"_index\":21,\"name\":{\"21\":{},\"67\":{},\"98\":{}},\"parent\":{}}],[\"navbar\",{\"_index\":16,\"name\":{\"16\":{}},\"parent\":{}}],[\"notfoundpage\",{\"_index\":116,\"name\":{\"111\":{}},\"parent\":{}}],[\"osocletters\",{\"_index\":17,\"name\":{\"17\":{}},\"parent\":{}}],[\"password\",{\"_index\":14,\"name\":{\"14\":{},\"22\":{}},\"parent\":{}}],[\"pendingpage\",{\"_index\":118,\"name\":{\"113\":{}},\"parent\":{}}],[\"pendingrequests\",{\"_index\":41,\"name\":{\"40\":{},\"41\":{}},\"parent\":{}}],[\"pendingrequestscomponents\",{\"_index\":43,\"name\":{\"42\":{}},\"parent\":{}}],[\"privateroute\",{\"_index\":18,\"name\":{\"18\":{}},\"parent\":{}}],[\"projectspage\",{\"_index\":119,\"name\":{\"114\":{}},\"parent\":{}}],[\"registercomponents\",{\"_index\":19,\"name\":{\"19\":{}},\"parent\":{}}],[\"registerpage\",{\"_index\":120,\"name\":{\"115\":{}},\"parent\":{}}],[\"rejectrequest\",{\"_index\":95,\"name\":{\"87\":{}},\"parent\":{}}],[\"removeadmin\",{\"_index\":84,\"name\":{\"77\":{}},\"parent\":{}}],[\"removeadminandcoach\",{\"_index\":85,\"name\":{\"78\":{}},\"parent\":{}}],[\"removecoach\",{\"_index\":34,\"name\":{\"34\":{}},\"parent\":{}}],[\"removecoachfromalleditions\",{\"_index\":89,\"name\":{\"82\":{}},\"parent\":{}}],[\"removecoachfromedition\",{\"_index\":88,\"name\":{\"81\":{}},\"parent\":{}}],[\"request\",{\"_index\":96,\"name\":{\"88\":{}},\"parent\":{}}],[\"requestfilter\",{\"_index\":46,\"name\":{\"44\":{}},\"parent\":{}}],[\"requestid\",{\"_index\":97,\"name\":{\"89\":{}},\"parent\":{}}],[\"requestlist\",{\"_index\":48,\"name\":{\"46\":{}},\"parent\":{}}],[\"requestlistitem\",{\"_index\":47,\"name\":{\"45\":{}},\"parent\":{}}],[\"requests\",{\"_index\":91,\"name\":{\"84\":{},\"92\":{}},\"parent\":{}}],[\"requestsheader\",{\"_index\":49,\"name\":{\"47\":{}},\"parent\":{}}],[\"role\",{\"_index\":54,\"name\":{\"51\":{},\"61\":{}},\"parent\":{}}],[\"router\",{\"_index\":2,\"name\":{\"2\":{}},\"parent\":{\"3\":{}}}],[\"setbearertoken\",{\"_index\":77,\"name\":{\"72\":{}},\"parent\":{}}],[\"seteditions\",{\"_index\":59,\"name\":{\"56\":{}},\"parent\":{}}],[\"setisloggedin\",{\"_index\":53,\"name\":{\"50\":{}},\"parent\":{}}],[\"setrole\",{\"_index\":55,\"name\":{\"52\":{}},\"parent\":{}}],[\"settoken\",{\"_index\":57,\"name\":{\"54\":{},\"108\":{}},\"parent\":{}}],[\"socialbuttons\",{\"_index\":13,\"name\":{\"13\":{},\"24\":{}},\"parent\":{}}],[\"storagekey\",{\"_index\":62,\"name\":{\"59\":{}},\"parent\":{}}],[\"studentspage\",{\"_index\":121,\"name\":{\"116\":{}},\"parent\":{}}],[\"token\",{\"_index\":56,\"name\":{\"53\":{}},\"parent\":{}}],[\"user\",{\"_index\":70,\"name\":{\"65\":{},\"90\":{},\"96\":{}},\"parent\":{}}],[\"userid\",{\"_index\":72,\"name\":{\"66\":{},\"97\":{}},\"parent\":{}}],[\"users\",{\"_index\":78,\"name\":{\"73\":{},\"93\":{},\"102\":{}},\"parent\":{}}],[\"userscomponents\",{\"_index\":25,\"name\":{\"27\":{}},\"parent\":{}}],[\"userslist\",{\"_index\":105,\"name\":{\"101\":{}},\"parent\":{}}],[\"userspage\",{\"_index\":122,\"name\":{\"117\":{}},\"parent\":{}}],[\"utils\",{\"_index\":6,\"name\":{\"7\":{}},\"parent\":{\"70\":{},\"106\":{}}}],[\"utils.api\",{\"_index\":76,\"name\":{},\"parent\":{\"71\":{},\"72\":{},\"73\":{}}}],[\"utils.api.users\",{\"_index\":80,\"name\":{},\"parent\":{\"74\":{},\"79\":{},\"84\":{},\"93\":{}}}],[\"utils.api.users.admins\",{\"_index\":82,\"name\":{},\"parent\":{\"75\":{},\"76\":{},\"77\":{},\"78\":{}}}],[\"utils.api.users.coaches\",{\"_index\":87,\"name\":{},\"parent\":{\"80\":{},\"81\":{},\"82\":{},\"83\":{}}}],[\"utils.api.users.requests\",{\"_index\":93,\"name\":{},\"parent\":{\"85\":{},\"86\":{},\"87\":{},\"88\":{},\"91\":{}}}],[\"utils.api.users.requests.getrequestsresponse\",{\"_index\":100,\"name\":{},\"parent\":{\"92\":{}}}],[\"utils.api.users.requests.request\",{\"_index\":98,\"name\":{},\"parent\":{\"89\":{},\"90\":{}}}],[\"utils.api.users.users\",{\"_index\":102,\"name\":{},\"parent\":{\"94\":{},\"95\":{},\"96\":{},\"101\":{},\"103\":{}}}],[\"utils.api.users.users.mailto\",{\"_index\":108,\"name\":{},\"parent\":{\"104\":{},\"105\":{}}}],[\"utils.api.users.users.user\",{\"_index\":104,\"name\":{},\"parent\":{\"97\":{},\"98\":{},\"99\":{},\"100\":{}}}],[\"utils.api.users.users.userslist\",{\"_index\":106,\"name\":{},\"parent\":{\"102\":{}}}],[\"utils.localstorage\",{\"_index\":112,\"name\":{},\"parent\":{\"107\":{},\"108\":{}}}],[\"validateregistrationurl\",{\"_index\":75,\"name\":{\"71\":{}},\"parent\":{}}],[\"verifyingtokenpage\",{\"_index\":123,\"name\":{\"118\":{}},\"parent\":{}}],[\"views\",{\"_index\":7,\"name\":{\"8\":{}},\"parent\":{\"109\":{},\"112\":{},\"113\":{},\"114\":{},\"115\":{},\"116\":{},\"117\":{},\"118\":{}}}],[\"views.errors\",{\"_index\":115,\"name\":{},\"parent\":{\"110\":{},\"111\":{}}}],[\"welcometext\",{\"_index\":11,\"name\":{\"12\":{}},\"parent\":{}}]],\"pipeline\":[]}}"); \ No newline at end of file diff --git a/frontend/docs/enums/data.Enums.Role.html b/frontend/docs/enums/data.Enums.Role.html index 1c6b477bf..c99995eda 100644 --- a/frontend/docs/enums/data.Enums.Role.html +++ b/frontend/docs/enums/data.Enums.Role.html @@ -1,4 +1,4 @@ Role | OSOC 3 - Frontend Documentation
                          Options
                          All
                          • Public
                          • Public/Protected
                          • All
                          Menu

                          Enum for the different levels of authority a user can have

                          -

                          Index

                          Enumeration members

                          Enumeration members

                          ADMIN = 0
                          COACH = 1

                          Legend

                          • Namespace
                          • Function
                          • Interface

                          Settings

                          Theme

                          \ No newline at end of file +

                        Index

                        Enumeration members

                        Enumeration members

                        ADMIN = 0
                        COACH = 1

                        Legend

                        • Namespace
                        • Function
                        • Interface

                        Settings

                        Theme

                        \ No newline at end of file diff --git a/frontend/docs/enums/data.Enums.StorageKey.html b/frontend/docs/enums/data.Enums.StorageKey.html index b2773b327..9f34a0076 100644 --- a/frontend/docs/enums/data.Enums.StorageKey.html +++ b/frontend/docs/enums/data.Enums.StorageKey.html @@ -1,5 +1,5 @@ StorageKey | OSOC 3 - Frontend Documentation
                        Options
                        All
                        • Public
                        • Public/Protected
                        • All
                        Menu

                        Enum for the keys in LocalStorage.

                        -

                        Index

                        Enumeration members

                        Enumeration members

                        BEARER_TOKEN = "bearerToken"
                        +

                        Index

                        Enumeration members

                        Enumeration members

                        BEARER_TOKEN = "bearerToken"

                        Bearer token used to authorize the user's requests in the backend.

                        Legend

                        • Namespace
                        • Function
                        • Interface

                        Settings

                        Theme

                        \ No newline at end of file diff --git a/frontend/docs/interfaces/contexts.AuthContextState.html b/frontend/docs/interfaces/contexts.AuthContextState.html index 62d82b8be..b0a687d84 100644 --- a/frontend/docs/interfaces/contexts.AuthContextState.html +++ b/frontend/docs/interfaces/contexts.AuthContextState.html @@ -1,3 +1,3 @@ AuthContextState | OSOC 3 - Frontend Documentation
                        Options
                        All
                        • Public
                        • Public/Protected
                        • All
                        Menu

                        Interface that holds the data stored in the AuthContext.

                        -

                        Hierarchy

                        • AuthContextState

                        Index

                        Properties

                        editions: number[]
                        isLoggedIn: null | boolean
                        role: null | Role
                        token: null | string

                        Methods

                        • setEditions(value: number[]): void
                        • setIsLoggedIn(value: null | boolean): void
                        • setRole(value: null | Role): void
                        • setToken(value: null | string): void

                        Legend

                        • Interface
                        • Property
                        • Method
                        • Namespace
                        • Function

                        Settings

                        Theme

                        \ No newline at end of file +

                      Hierarchy

                      • AuthContextState

                      Index

                      Properties

                      editions: number[]
                      isLoggedIn: null | boolean
                      role: null | Role
                      token: null | string

                      Methods

                      • setEditions(value: number[]): void
                      • setIsLoggedIn(value: null | boolean): void
                      • setRole(value: null | Role): void
                      • setToken(value: null | string): void

                      Legend

                      • Interface
                      • Property
                      • Method
                      • Namespace
                      • Function

                      Settings

                      Theme

                      \ No newline at end of file diff --git a/frontend/docs/interfaces/data.Interfaces.User.html b/frontend/docs/interfaces/data.Interfaces.User.html index bd5f08a50..acf3da32e 100644 --- a/frontend/docs/interfaces/data.Interfaces.User.html +++ b/frontend/docs/interfaces/data.Interfaces.User.html @@ -2,4 +2,4 @@

                      Data about a user using the application. Contains a list of edition names so that we can quickly check if they have access to a route or not.

                      -

                    Hierarchy

                    • User

                    Index

                    Properties

                    admin: boolean
                    editions: string[]
                    name: string
                    userId: number

                    Legend

                    • Namespace
                    • Function
                    • Interface
                    • Property

                    Settings

                    Theme

                    \ No newline at end of file +

                  Hierarchy

                  • User

                  Index

                  Properties

                  admin: boolean
                  editions: string[]
                  name: string
                  userId: number

                  Legend

                  • Namespace
                  • Function
                  • Interface
                  • Property

                  Settings

                  Theme

                  \ No newline at end of file diff --git a/frontend/docs/modules/App.html b/frontend/docs/modules/App.html index eb64b40b8..c6cd51e1e 100644 --- a/frontend/docs/modules/App.html +++ b/frontend/docs/modules/App.html @@ -1,4 +1,4 @@ -App | OSOC 3 - Frontend Documentation
                  Options
                  All
                  • Public
                  • Public/Protected
                  • All
                  Menu

                  Index

                  Functions

                  Functions

                  • default(): Element
                  • +App | OSOC 3 - Frontend Documentation
                    Options
                    All
                    • Public
                    • Public/Protected
                    • All
                    Menu

                    Index

                    Functions

                    Functions

                    • default(): Element

                    Legend

                    • Namespace
                    • Function
                    • Interface

                    Settings

                    Theme

                    \ No newline at end of file diff --git a/frontend/docs/modules/Router.html b/frontend/docs/modules/Router.html index 1f31876d9..1cebbc14d 100644 --- a/frontend/docs/modules/Router.html +++ b/frontend/docs/modules/Router.html @@ -1,4 +1,4 @@ -Router | OSOC 3 - Frontend Documentation
                    Options
                    All
                    • Public
                    • Public/Protected
                    • All
                    Menu

                    Index

                    Functions

                    Functions

                    • default(): Element
                    • +Router | OSOC 3 - Frontend Documentation
                      Options
                      All
                      • Public
                      • Public/Protected
                      • All
                      Menu

                      Index

                      Functions

                      Functions

                      • default(): Element
                      • Router component to render different pages depending on the current url. Renders the VerifyingTokenPage if the bearer token is still being validated.

                        Returns Element

                      Legend

                      • Namespace
                      • Function
                      • Interface

                      Settings

                      Theme

                      \ No newline at end of file diff --git a/frontend/docs/modules/components.LoginComponents.html b/frontend/docs/modules/components.LoginComponents.html index 1d7aaab6b..4f69c1b8b 100644 --- a/frontend/docs/modules/components.LoginComponents.html +++ b/frontend/docs/modules/components.LoginComponents.html @@ -1,9 +1,9 @@ -LoginComponents | OSOC 3 - Frontend Documentation
                      Options
                      All
                      • Public
                      • Public/Protected
                      • All
                      Menu

                      Index

                      Functions

                      • Email(__namedParameters: { email: string; setEmail: any }): Element
                      • +LoginComponents | OSOC 3 - Frontend Documentation
                        Options
                        All
                        • Public
                        • Public/Protected
                        • All
                        Menu

                        Index

                        Functions

                        • Email(__namedParameters: { email: string; setEmail: any }): Element
                        • Password(__namedParameters: { password: string; callLogIn: any; setPassword: any }): Element
                        • Password(__namedParameters: { password: string; callLogIn: any; setPassword: any }): Element
                        • SocialButtons(): Element
                        • SocialButtons(): Element
                        • WelcomeText(): Element
                        • WelcomeText(): Element

                        Legend

                        • Namespace
                        • Function
                        • Interface

                        Settings

                        Theme

                        \ No newline at end of file diff --git a/frontend/docs/modules/components.RegisterComponents.html b/frontend/docs/modules/components.RegisterComponents.html index 1c565b8bd..3a932f834 100644 --- a/frontend/docs/modules/components.RegisterComponents.html +++ b/frontend/docs/modules/components.RegisterComponents.html @@ -1,15 +1,15 @@ -RegisterComponents | OSOC 3 - Frontend Documentation
                        Options
                        All
                        • Public
                        • Public/Protected
                        • All
                        Menu

                        Index

                        Functions

                        • BadInviteLink(): Element
                        • SocialButtons(): Element

                        Legend

                        • Namespace
                        • Function
                        • Interface

                        Settings

                        Theme

                        \ No newline at end of file diff --git a/frontend/docs/modules/components.html b/frontend/docs/modules/components.html index a9e07fec7..0fda00ee7 100644 --- a/frontend/docs/modules/components.html +++ b/frontend/docs/modules/components.html @@ -1,23 +1,23 @@ -components | OSOC 3 - Frontend Documentation
                        Options
                        All
                        • Public
                        • Public/Protected
                        • All
                        Menu

                        Index

                        Functions

                        • AdminRoute(): Element
                        • +components | OSOC 3 - Frontend Documentation
                          Options
                          All
                          • Public
                          • Public/Protected
                          • All
                          Menu

                          Index

                          Functions

                          • AdminRoute(): Element
                          • React component for admin-only routes. Redirects to the LoginPage (status 401) if not authenticated, and to the ForbiddenPage (status 403) if not admin.

                            Example usage:

                            <Route path={"/path"} element={<AdminRoute />}>
                            // These routes will only render if the user is an admin
                            <Route path={"/"} />
                            <Route path={"/child"} />
                            </Route>
                            -

                            Returns Element

                          • Footer(): Element
                          • Footer(): Element
                          • Footer placed at the bottom of the site, containing various links related to the application or our code.

                            The footer is only displayed when signed in.

                            -

                            Returns Element

                          • NavBar(): Element
                          • NavBar(): Element
                          • NavBar displayed at the top of the page. Links are hidden if the user is not authorized to see them.

                            -

                            Returns Element

                          • OSOCLetters(): Element
                          • OSOCLetters(): Element
                          • Animated OSOC-letters, inspired by the ones found on the OSOC website.

                            Note: This component is currently not in use because the positioning of the letters causes issues. We have given priority to other parts of the application.

                            -

                            Returns Element

                          • PrivateRoute(): Element
                          • PrivateRoute(): Element
                          • React component that redirects to the LoginPage if not authenticated when trying to visit a route.

                            Example usage:

                            diff --git a/frontend/docs/modules/contexts.html b/frontend/docs/modules/contexts.html index 10893ba07..e683770f6 100644 --- a/frontend/docs/modules/contexts.html +++ b/frontend/docs/modules/contexts.html @@ -1,4 +1,4 @@ -contexts | OSOC 3 - Frontend Documentation
                            Options
                            All
                            • Public
                            • Public/Protected
                            • All
                            Menu

                            Index

                            Interfaces

                            Functions

                            Functions

                            • AuthProvider(__namedParameters: { children: ReactNode }): Element
                            • +contexts | OSOC 3 - Frontend Documentation
                              Options
                              All
                              • Public
                              • Public/Protected
                              • All
                              Menu

                              Index

                              Interfaces

                              Functions

                              Functions

                              • AuthProvider(__namedParameters: { children: ReactNode }): Element
                              • Provider for auth that creates getters, setters, maintains state, and provides default values.

                                This keeps the main App component code clean by handling this diff --git a/frontend/docs/modules/utils.Api.html b/frontend/docs/modules/utils.Api.html index 0aacecc7d..4b8922a07 100644 --- a/frontend/docs/modules/utils.Api.html +++ b/frontend/docs/modules/utils.Api.html @@ -1,7 +1,7 @@ -Api | OSOC 3 - Frontend Documentation

                                Options
                                All
                                • Public
                                • Public/Protected
                                • All
                                Menu

                                Index

                                Functions

                                • setBearerToken(value: null | string): void
                                • +Api | OSOC 3 - Frontend Documentation
                                  Options
                                  All
                                  • Public
                                  • Public/Protected
                                  • All
                                  Menu

                                  Index

                                  Functions

                                  • setBearerToken(value: null | string): void
                                  • Function to set the default bearer token in the request headers. Passing null as the value will remove the header instead.

                                    -

                                    Parameters

                                    • value: null | string

                                    Returns void

                                  • validateRegistrationUrl(edition: string, uuid: string): Promise<boolean>
                                  • validateRegistrationUrl(edition: string, uuid: string): Promise<boolean>
                                  • Function to check if a registration url exists by sending a GET request, if this returns a 200 then we know the url is valid.

                                    Parameters

                                    • edition: string
                                    • uuid: string

                                    Returns Promise<boolean>

                                  Legend

                                  • Namespace
                                  • Function
                                  • Interface

                                  Settings

                                  Theme

                                  \ No newline at end of file diff --git a/frontend/docs/modules/utils.LocalStorage.html b/frontend/docs/modules/utils.LocalStorage.html index 9d18be5b2..25adcc310 100644 --- a/frontend/docs/modules/utils.LocalStorage.html +++ b/frontend/docs/modules/utils.LocalStorage.html @@ -1,6 +1,6 @@ -LocalStorage | OSOC 3 - Frontend Documentation
                                  Options
                                  All
                                  • Public
                                  • Public/Protected
                                  • All
                                  Menu

                                  Index

                                  Functions

                                  • getToken(): string | null
                                  • +LocalStorage | OSOC 3 - Frontend Documentation
                                    Options
                                    All
                                    • Public
                                    • Public/Protected
                                    • All
                                    Menu

                                    Index

                                    Functions

                                    • getToken(): string | null
                                    • Function to pull the user's token out of LocalStorage. Returns null if there is no token in LocalStorage yet.

                                      -

                                      Returns string | null

                                    • setToken(value: null | string): void
                                    • setToken(value: null | string): void
                                    • Function to set a new value for the bearer token in LocalStorage.

                                      Parameters

                                      • value: null | string

                                      Returns void

                                    Legend

                                    • Namespace
                                    • Function
                                    • Interface

                                    Settings

                                    Theme

                                    \ No newline at end of file diff --git a/frontend/docs/modules/views.Errors.html b/frontend/docs/modules/views.Errors.html index 667e202c5..9aa6cea26 100644 --- a/frontend/docs/modules/views.Errors.html +++ b/frontend/docs/modules/views.Errors.html @@ -1,7 +1,7 @@ -Errors | OSOC 3 - Frontend Documentation
                                    Options
                                    All
                                    • Public
                                    • Public/Protected
                                    • All
                                    Menu

                                    Index

                                    Functions

                                    • ForbiddenPage(): Element
                                    • +Errors | OSOC 3 - Frontend Documentation
                                      Options
                                      All
                                      • Public
                                      • Public/Protected
                                      • All
                                      Menu

                                      Index

                                      Functions

                                      • ForbiddenPage(): Element
                                      • Page shown to users when they try to access a resource they aren't authorized to. Examples include coaches performing admin actions, or coaches going to urls for editions they aren't part of.

                                        -

                                        Returns Element

                                      • NotFoundPage(): Element
                                      • NotFoundPage(): Element

                                      Legend

                                      • Namespace
                                      • Function
                                      • Interface

                                      Settings

                                      Theme

                                      \ No newline at end of file diff --git a/frontend/docs/modules/views.html b/frontend/docs/modules/views.html index e0ccf329b..e2f0f6543 100644 --- a/frontend/docs/modules/views.html +++ b/frontend/docs/modules/views.html @@ -1,14 +1,14 @@ -views | OSOC 3 - Frontend Documentation
                                      Options
                                      All
                                      • Public
                                      • Public/Protected
                                      • All
                                      Menu

                                      Index

                                      Functions

                                      • LoginPage(): Element
                                      • +views | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        Index

                                        Functions

                                        • LoginPage(): Element
                                        • PendingPage(): Element
                                        • PendingPage(): Element
                                        • ProjectsPage(): Element
                                        • RegisterPage(): Element
                                        • ProjectsPage(): Element
                                        • RegisterPage(): Element
                                        • StudentsPage(): Element
                                        • UsersPage(): Element
                                        • StudentsPage(): Element
                                        • UsersPage(): Element
                                        • VerifyingTokenPage(): Element
                                        • VerifyingTokenPage(): Element

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/src/components/UsersComponents/Coaches/Coaches.tsx b/frontend/src/components/UsersComponents/Coaches/Coaches.tsx index a7c17f082..c5adb1a70 100644 --- a/frontend/src/components/UsersComponents/Coaches/Coaches.tsx +++ b/frontend/src/components/UsersComponents/Coaches/Coaches.tsx @@ -6,18 +6,18 @@ import { getCoaches } from "../../../utils/api/users/coaches"; import { CoachList, AddCoach } from "./CoachesComponents"; /** - * - * @param props - * @constructor + * 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 need to be shown. */ export default function Coaches(props: { edition: string }) { - const [allCoaches, setAllCoaches] = useState([]); - const [coaches, setCoaches] = useState([]); - const [users, setUsers] = useState([]); - const [gettingData, setGettingData] = useState(false); - const [searchTerm, setSearchTerm] = useState(""); - const [gotData, setGotData] = useState(false); - const [error, setError] = useState(""); + const [allCoaches, setAllCoaches] = useState([]); // All coaches from the edition + const [coaches, setCoaches] = useState([]); // All coaches after filter + const [users, setUsers] = useState([]); // All users which are not a coach + const [gettingData, setGettingData] = useState(false); // Waiting for data + const [searchTerm, setSearchTerm] = useState(""); // The word set in filter + const [gotData, setGotData] = useState(false); // Received data + const [error, setError] = useState(""); // Error message async function getData() { setGettingData(true); @@ -50,6 +50,11 @@ export default function Coaches(props: { edition: string }) { } }, [gotData, gettingData, error, getData]); + /** + * Apply a filter to the coach list. + * Only keep coaches who's name contain the searchterm. + * @param {string} word a searchterm which a coach needs to contain + */ const filter = (word: string) => { setSearchTerm(word); const newCoaches: User[] = []; diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx index c3aa6c801..828704b7a 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx @@ -6,6 +6,13 @@ import { Button, Modal } from "react-bootstrap"; import { Typeahead } from "react-bootstrap-typeahead"; import { Error } from "../../PendingRequests/styles"; +/** + * A button and popup to add a new coach to the given edition. + * The popup consists of a field to search for a user. + * @param props.users A list of all users which can be added as coach to the edition. + * @param props.edition The edition to which users need to be added. + * @param props.refresh A function which will be called when a user is added as coach. + */ export default function AddCoach(props: { users: User[]; edition: string; refresh: () => void }) { const [show, setShow] = useState(false); const [selected, setSelected] = useState(undefined); diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx index 80ed5adf0..476862e2f 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx @@ -3,8 +3,16 @@ import { SpinnerContainer } from "../../PendingRequests/styles"; import { Spinner } from "react-bootstrap"; import { CoachesTable } from "../styles"; import React from "react"; -import { CoachItem } from "./index"; +import { CoachListItem } from "./index"; +/** + * 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.refresh A function which will be called when a coach is removed. + */ export default function CoachList(props: { coaches: User[]; loading: boolean; @@ -29,7 +37,7 @@ export default function CoachList(props: { const body = ( {props.coaches.map(coach => ( - void }) { const [show, setShow] = useState(false); const [error, setError] = useState(""); diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/index.ts b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/index.ts index 342f63cd7..94422da8c 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/index.ts +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/index.ts @@ -1,4 +1,4 @@ export { default as AddCoach } from "./AddCoach"; -export { default as CoachItem } from "./CoachListItem"; +export { default as CoachListItem } from "./CoachListItem"; export { default as CoachList } from "./CoachList"; export { default as RemoveCoach } from "./RemoveCoach"; diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx index f8988ebb2..57d83661c 100644 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx @@ -2,19 +2,23 @@ import React, { useState } from "react"; import { getInviteLink } from "../../../utils/api/users/users"; import "./InviteUser.css"; import { InviteInput, InviteContainer } from "./styles"; -import { ButtonsDiv, ErrorDiv, LinkDiv } from "./InviteUserComponents"; +import { ButtonsDiv, ErrorDiv } from "./InviteUserComponents"; +/** + * A component to invite a user as coach to a given edition. + * Contains an input field for the email address of the new user + * and a button to get a mailto link which contains the invite link or just the invite link. + * @param props.edition The edition whereto the person will be invited. + */ export default function InviteUser(props: { edition: string }) { - const [email, setEmail] = useState(""); - const [valid, setValid] = useState(true); - const [errorMessage, setErrorMessage] = useState(""); - const [loading, setLoading] = useState(false); - const [link, setLink] = useState(""); + 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 changeEmail = function (email: string) { setEmail(email); setValid(true); - setLink(""); setErrorMessage(""); }; @@ -51,7 +55,6 @@ export default function InviteUser(props: { edition: string }) { -
                                        ); } diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ButtonsDiv.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ButtonsDiv.tsx index 76b9458b6..0ad3a1373 100644 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ButtonsDiv.tsx +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ButtonsDiv.tsx @@ -1,6 +1,11 @@ import { CopyButton, InviteButton, Loader } from "../styles"; import React from "react"; +/** + * A component to choice between sending an invite or copying it to clipboard. + * @param props.loading Invite is being created. + * @param props.sendInvite A function to send/copy the link. + */ export default function ButtonsDiv(props: { loading: boolean; sendInvite: (copy: boolean) => void; diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ErrorDiv.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ErrorDiv.tsx index b58b35ccb..149413d6d 100644 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ErrorDiv.tsx +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ErrorDiv.tsx @@ -1,6 +1,10 @@ import { Error } from "../styles"; import React from "react"; +/** + * A component which shows an error if there is one. + * @param props.errorMessage The possible message. + */ export default function ErrorDiv(props: { errorMessage: string }) { let errorDiv = null; if (props.errorMessage) { diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/LinkDiv.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/LinkDiv.tsx deleted file mode 100644 index 8065ae6ad..000000000 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/LinkDiv.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Link } from "../styles"; -import React from "react"; - -export default function LinkDiv(props: { link: string }) { - let linkDiv = null; - if (props.link) { - linkDiv = {props.link}; - } - return linkDiv; -} diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/index.ts b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/index.ts index 8542f73b7..6d540a8e1 100644 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/index.ts +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/index.ts @@ -1,3 +1,2 @@ export { default as ButtonsDiv } from "./ButtonsDiv"; export { default as ErrorDiv } from "./ErrorDiv"; -export { default as LinkDiv } from "./LinkDiv"; diff --git a/frontend/src/components/UsersComponents/InviteUser/styles.ts b/frontend/src/components/UsersComponents/InviteUser/styles.ts index a48b94985..7a70712e0 100644 --- a/frontend/src/components/UsersComponents/InviteUser/styles.ts +++ b/frontend/src/components/UsersComponents/InviteUser/styles.ts @@ -62,10 +62,6 @@ export const Loader = styled.div` float: left; `; -export const Link = styled.div` - margin-left: 10px; -`; - export const Error = styled.div` margin-left: 10px; color: var(--osoc_red); diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx index 97eede932..22dfe8aa6 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx @@ -2,20 +2,21 @@ import React, { useEffect, useState } from "react"; import Collapsible from "react-collapsible"; import { PendingRequestsContainer, Error } from "./styles"; import { getRequests, Request } from "../../../utils/api/users/requests"; -import { RequestFilter, RequestsHeader } from "./PendingRequestsComponents"; - -function RequestsList(props: { loading: boolean; gotData: boolean; requests: Request[] }) { - return null; -} +import { RequestFilter, RequestList, RequestsHeader } from "./PendingRequestsComponents"; +/** + * A collapsible component which contains all coach requests for a given edition. + * Every request can be accepted or rejected. + * @param props.edition The edition. + */ export default function PendingRequests(props: { edition: string }) { - const [allRequests, setAllRequests] = useState([]); - const [requests, setRequests] = useState([]); - const [gettingRequests, setGettingRequests] = useState(false); - const [searchTerm, setSearchTerm] = useState(""); - const [gotData, setGotData] = useState(false); - const [open, setOpen] = useState(false); - const [error, setError] = useState(""); + const [allRequests, setAllRequests] = useState([]); // All requests for the given edition + const [requests, setRequests] = useState([]); // All requests after filter + const [gettingRequests, setGettingRequests] = useState(false); // Waiting for data + const [searchTerm, setSearchTerm] = useState(""); // The word set in filter + const [gotData, setGotData] = useState(false); // Received data + const [open, setOpen] = useState(false); // Collapsible is open + const [error, setError] = useState(""); // Error message async function getData() { try { @@ -60,7 +61,7 @@ export default function PendingRequests(props: { edition: string }) { filter={word => filter(word)} show={allRequests.length > 0} /> - + {error} diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/AcceptReject.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/AcceptReject.tsx index 3149ff550..823697161 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/AcceptReject.tsx +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/AcceptReject.tsx @@ -2,6 +2,10 @@ import { AcceptButton, RejectButton } from "../styles"; import { acceptRequest, rejectRequest } from "../../../../utils/api/users/requests"; import React from "react"; +/** + * Component consisting of two buttons to accept or reject a coach request. + * @param props.requestId The id of the request. + */ export default function AcceptReject(props: { requestId: number }) { return (
                                        diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestFilter.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestFilter.tsx index 59a555752..960a07c6d 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestFilter.tsx +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestFilter.tsx @@ -1,6 +1,12 @@ import { SearchInput } from "../styles"; import React from "react"; +/** + * Input field to filter the [[RequestList]]. + * @param props.searchTerm The word in the input filed. + * @param props.filter A function to change the search term. + * @param props.show Boolean to reflect if the component needs to be shown. + */ export default function RequestFilter(props: { searchTerm: string; filter: (key: string) => void; diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx index 9965484ba..17589f8ed 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx @@ -4,6 +4,12 @@ import { Spinner } from "react-bootstrap"; import React from "react"; import RequestListItem from "./RequestListItem"; +/** + * A list of [[RequestListItem]]s. + * @param props.requests A list of requests which need to be shown. + * @param props.loading Waiting for data. + * @param props.gotData Data is received. + */ export default function RequestList(props: { requests: Request[]; loading: boolean; diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestListItem.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestListItem.tsx index dd6bcb2c0..820476dbc 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestListItem.tsx +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestListItem.tsx @@ -2,6 +2,11 @@ import { Request } from "../../../../utils/api/users/requests"; import React from "react"; import AcceptReject from "./AcceptReject"; +/** + * An item from [[RequestList]] which represents one request. + * This includes two buttons to accept and reject the request. + * @param props.request The request which is represented. + */ export default function RequestListItem(props: { request: Request }) { return ( diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestsHeader.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestsHeader.tsx index e87e73e1f..df2c0ba42 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestsHeader.tsx +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestsHeader.tsx @@ -1,6 +1,10 @@ import { ClosedArrow, OpenArrow, RequestHeaderDiv, RequestHeaderTitle } from "../styles"; import React from "react"; +/** + * Arrow to indicate the status of the collapsible component. + * @param props.open Boolean to indicate if the collapsible is open. + */ function Arrow(props: { open: boolean }) { if (props.open) { return ; @@ -9,6 +13,10 @@ function Arrow(props: { open: boolean }) { } } +/** + * The header of [[PendingRequests]]. + * @param props.open Boolean to indicate if the collapsible is open. + */ export default function RequestsHeader(props: { open: boolean }) { return ( From 84e7b5283cc202d2074ddbf5c1acf27fd63f3dfe Mon Sep 17 00:00:00 2001 From: Francis Date: Mon, 4 Apr 2022 12:52:31 +0200 Subject: [PATCH 225/536] Fix typing in webhooks --- backend/src/app/logic/webhooks.py | 34 +++++++++++++++----------- backend/src/app/routers/users/users.py | 2 +- backend/src/app/schemas/webhooks.py | 1 - 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/backend/src/app/logic/webhooks.py b/backend/src/app/logic/webhooks.py index 4e0c3184d..15f89db6f 100644 --- a/backend/src/app/logic/webhooks.py +++ b/backend/src/app/logic/webhooks.py @@ -1,9 +1,11 @@ +from typing import cast + import sqlalchemy.exc from sqlalchemy.orm import Session from settings import FormMapping from src.app.exceptions.webhooks import WebhookProcessException -from src.app.schemas.webhooks import WebhookEvent, Question, Form +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 @@ -18,10 +20,9 @@ def process_webhook(edition: Edition, data: WebhookEvent, database: Session): questions: list[Question] = form.fields extra_questions: list[Question] = [] - attributes: dict[str, str | int] = {'edition': edition} + attributes: dict = {'edition': edition} for question in questions: - question: Question match FormMapping(question.key): case FormMapping.FIRST_NAME: attributes['first_name'] = question.value @@ -34,10 +35,11 @@ def process_webhook(edition: Edition, data: WebhookEvent, database: Session): case FormMapping.PHONE_NUMBER: attributes['phone_number'] = question.value case FormMapping.STUDENT_COACH: - for option in question.options: - if option.id == question.value: - attributes['wants_to_be_student_coach'] = "yes" in option.text.lower() - break # Only 2 options, Yes and No. + if question.options is not None: + for option in question.options: + if option.id == question.value: + attributes['wants_to_be_student_coach'] = "yes" in option.text.lower() + break # Only 2 options, Yes and No. case _: extra_questions.append(question) @@ -76,7 +78,7 @@ def process_remaining_questions(student: Student, questions: list[Question], dat continue model = QuestionModel( - type=question.type, + type=QE(question.type), question=question.label, student=student ) @@ -85,8 +87,9 @@ def process_remaining_questions(student: Student, questions: list[Question], dat match QE(question.type): case QE.MULTIPLE_CHOICE: - value: str = question.value - for option in question.options: + value: str = cast(str, question.value) + options = cast(list[QuestionOption], question.options) + for option in options: if option.id == value: database.add(QuestionAnswer( answer=option.text, @@ -96,12 +99,13 @@ def process_remaining_questions(student: Student, questions: list[Question], dat case QE.INPUT_EMAIL | QE.INPUT_LINK | QE.INPUT_TEXT | QE.TEXTAREA | QE.INPUT_PHONE_NUMBER | QE.INPUT_NUMBER: if question.value: database.add(QuestionAnswer( - answer=question.value, + answer=cast(str, question.value), question=model )) case QE.FILE_UPLOAD: if question.value: - for upload in question.value: + uploads = cast(list[QuestionUpload], question.value) + for upload in uploads: database.add(QuestionFileAnswer( file_name=upload.name, url=upload.url, @@ -110,8 +114,10 @@ def process_remaining_questions(student: Student, questions: list[Question], dat question=model )) case QE.CHECKBOXES: - for value in question.value: - for option in question.options: + answers = cast(list[str], question.value) + for value in answers: + options = cast(list[QuestionOption], question.options) + for option in options: if option.id == value: database.add(QuestionAnswer( answer=option.text, diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index 652d6e725..c67f3fd3b 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Query, Depends -from requests import Session +from sqlalchemy.orm import Session from src.app.routers.tags import Tags import src.app.logic.users as logic diff --git a/backend/src/app/schemas/webhooks.py b/backend/src/app/schemas/webhooks.py index 2763da41d..a71c4692d 100644 --- a/backend/src/app/schemas/webhooks.py +++ b/backend/src/app/schemas/webhooks.py @@ -45,7 +45,6 @@ class Form(CamelCaseModel): """The form data containing all the questions""" response_id: str submission_id: str - response_id: str form_id: str form_name: str created_at: str From a6c83a5db925889fbf75f3f028dcb7b123ab5a0a Mon Sep 17 00:00:00 2001 From: Francis Date: Mon, 4 Apr 2022 12:57:06 +0200 Subject: [PATCH 226/536] Fix typing in security --- backend/src/app/logic/security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/logic/security.py b/backend/src/app/logic/security.py index 32aed5bfb..5332b7081 100644 --- a/backend/src/app/logic/security.py +++ b/backend/src/app/logic/security.py @@ -56,7 +56,7 @@ def authenticate_user(db: Session, email: str, password: str) -> models.User: """Match an email/password combination to a User model""" user = get_user_by_email(db, email) - if not verify_password(password, user.email_auth.pw_hash): + if user.email_auth.pw_hash is None or not verify_password(password, user.email_auth.pw_hash): raise InvalidCredentialsException() return user From 093060929aeafa85a2d98d05d4f0b1f49b853fc3 Mon Sep 17 00:00:00 2001 From: Francis Date: Mon, 4 Apr 2022 13:09:58 +0200 Subject: [PATCH 227/536] Fix typing --- backend/src/app/logic/editions.py | 9 +++++---- backend/src/app/logic/projects.py | 4 ++-- backend/src/app/logic/skills.py | 2 +- backend/src/app/routers/editions/projects/projects.py | 6 +++--- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/backend/src/app/logic/editions.py b/backend/src/app/logic/editions.py index 43969ef13..39fd69010 100644 --- a/backend/src/app/logic/editions.py +++ b/backend/src/app/logic/editions.py @@ -1,8 +1,9 @@ from sqlalchemy.orm import Session +import src.database.crud.editions as crud_editions from src.app.schemas.editions import Edition, EditionBase, EditionList -import src.database.crud.editions as crud_editions -from src.database.models import Edition +from src.database.models import Edition as EditionModel + def get_editions(db: Session) -> EditionList: """Get a list of all editions. @@ -16,7 +17,7 @@ def get_editions(db: Session) -> EditionList: return EditionList(editions=crud_editions.get_editions(db)) -def get_edition_by_id(db: Session, edition_id: int) -> Edition: +def get_edition_by_id(db: Session, edition_id: int) -> EditionModel: """Get a specific edition. Args: @@ -28,7 +29,7 @@ def get_edition_by_id(db: Session, edition_id: int) -> Edition: return crud_editions.get_edition_by_id(db, edition_id) -def create_edition(db: Session, edition: EditionBase) -> Edition: +def create_edition(db: Session, edition: EditionBase) -> EditionModel: """ Create a new edition. Args: diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index 7ad92f373..2638ee3bd 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -3,7 +3,7 @@ from src.app.schemas.projects import ProjectList, Project, ConflictStudentList from src.database.crud.projects import db_get_all_projects, db_add_project, db_delete_project, \ db_patch_project, db_get_conflict_students -from src.database.models import Edition +from src.database.models import Edition, Project as ProjectModel def logic_get_project_list(db: Session, edition: Edition) -> ProjectList: @@ -26,7 +26,7 @@ def logic_delete_project(db: Session, project_id: int): db_delete_project(db, project_id) -def logic_patch_project(db: Session, project: Project, name: str, number_of_students: int, skills: list[int], +def logic_patch_project(db: Session, project: ProjectModel, name: str, number_of_students: int, skills: list[int], partners: list[str], coaches: list[int]): """Make changes to a project""" db_patch_project(db, project, name, number_of_students, skills, partners, coaches) diff --git a/backend/src/app/logic/skills.py b/backend/src/app/logic/skills.py index 562916c3f..00825ba52 100644 --- a/backend/src/app/logic/skills.py +++ b/backend/src/app/logic/skills.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session -from src.app.schemas.skills import Skill, SkillBase, SkillList +from src.app.schemas.skills import SkillBase, SkillList import src.database.crud.skills as crud_skills from src.database.models import Skill diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index d8bdf6406..b98aeade2 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -10,7 +10,7 @@ ConflictStudentList from src.app.utils.dependencies import get_edition, get_project from src.database.database import get_session -from src.database.models import Edition +from src.database.models import Edition, Project as ProjectModel from .students import project_students_router projects_router = APIRouter(prefix="/projects", tags=[Tags.PROJECTS]) @@ -55,7 +55,7 @@ async def delete_project(project_id: int, db: Session = Depends(get_session)): @projects_router.get("/{project_id}", status_code=status.HTTP_200_OK, response_model=Project) -async def get_project(project: Project = Depends(get_project)): +async def get_project_route(project: ProjectModel = Depends(get_project)): """ Get information about a specific project. """ @@ -63,7 +63,7 @@ async def get_project(project: Project = Depends(get_project)): @projects_router.patch("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response) -async def patch_project(input_project: InputProject, project: Project = Depends(get_project), +async def patch_project(input_project: InputProject, project: ProjectModel = Depends(get_project), db: Session = Depends(get_session)): """ Update a project, changing some fields. From 0a0850db927b73778bb168caa271ec213541224f Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 4 Apr 2022 13:14:46 +0200 Subject: [PATCH 228/536] Make POST /invites return the generated link --- backend/src/app/logic/invites.py | 8 ++++---- backend/src/app/routers/editions/invites/invites.py | 4 ++-- backend/src/app/schemas/invites.py | 7 +++++-- .../test_editions/test_invites/test_invites.py | 1 + 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/backend/src/app/logic/invites.py b/backend/src/app/logic/invites.py index d1812e609..b5b98abdc 100644 --- a/backend/src/app/logic/invites.py +++ b/backend/src/app/logic/invites.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session -from src.app.schemas.invites import InvitesListResponse, EmailAddress, MailtoLink, InviteLink as InviteLinkModel +from src.app.schemas.invites import InvitesListResponse, EmailAddress, NewInviteLink, InviteLink as InviteLinkModel from src.app.utils.mailto import generate_mailto_string from src.database.crud.invites import create_invite_link, delete_invite_link as delete_link_db, get_all_pending_invites from src.database.models import Edition, InviteLink as InviteLinkDB @@ -27,7 +27,7 @@ def get_pending_invites_list(db: Session, edition: Edition) -> InvitesListRespon return InvitesListResponse(invite_links=invites) -def create_mailto_link(db: Session, edition: Edition, email_address: EmailAddress) -> MailtoLink: +def create_mailto_link(db: Session, edition: Edition, email_address: EmailAddress) -> NewInviteLink: """Add a new invite link into the database & return a mailto link for it""" # Create db entry new_link_db = create_invite_link(db, edition, email_address.email) @@ -35,7 +35,7 @@ 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/{new_link_db.uuid}" - return MailtoLink(mail_to=generate_mailto_string( + return NewInviteLink(mail_to=generate_mailto_string( recipient=email_address.email, subject=f"Open Summer Of Code {edition.year} invitation", body=link - )) + ), invite_link=link) diff --git a/backend/src/app/routers/editions/invites/invites.py b/backend/src/app/routers/editions/invites/invites.py index f8556000f..e500491c9 100644 --- a/backend/src/app/routers/editions/invites/invites.py +++ b/backend/src/app/routers/editions/invites/invites.py @@ -5,7 +5,7 @@ from src.app.logic.invites import create_mailto_link, delete_invite_link, get_pending_invites_list from src.app.routers.tags import Tags -from src.app.schemas.invites import InvitesListResponse, EmailAddress, MailtoLink, InviteLink as InviteLinkModel +from src.app.schemas.invites import InvitesListResponse, EmailAddress, NewInviteLink, InviteLink as InviteLinkModel from src.app.utils.dependencies import get_edition, get_invite_link, require_admin from src.database.database import get_session from src.database.models import Edition, InviteLink as InviteLinkDB @@ -21,7 +21,7 @@ async def get_invites(db: Session = Depends(get_session), edition: Edition = Dep return get_pending_invites_list(db, edition) -@invites_router.post("/", status_code=status.HTTP_201_CREATED, response_model=MailtoLink, +@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_edition)): diff --git a/backend/src/app/schemas/invites.py b/backend/src/app/schemas/invites.py index 13d32031a..cbd6327f5 100644 --- a/backend/src/app/schemas/invites.py +++ b/backend/src/app/schemas/invites.py @@ -41,6 +41,9 @@ class InvitesListResponse(CamelCaseModel): invite_links: list[InviteLink] -class MailtoLink(CamelCaseModel): - """A response containing a mailto link to invite a user with""" +class NewInviteLink(CamelCaseModel): + """A response containing a mailto link to invite a user with + Also contains the regular link in case the user wants to invite manually + """ mail_to: str + invite_link: str 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 76ecfa684..4f06741e4 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 @@ -53,6 +53,7 @@ def test_create_invite_valid(database_session: Session, auth_client: AuthClient) 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() From ef246ba557cdd8c63bb14a00928e6ebda45b5aa8 Mon Sep 17 00:00:00 2001 From: Francis Date: Mon, 4 Apr 2022 13:18:00 +0200 Subject: [PATCH 229/536] fix typing in projects --- backend/src/database/crud/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index 0daf06311..f9d4233b1 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -77,7 +77,7 @@ def db_patch_project(db: Session, project_id: int, input_project: InputProject): db.commit() -def db_get_conflict_students(db: Session, edition: Edition) -> list[(Student, list[Project])]: +def db_get_conflict_students(db: Session, edition: Edition) -> list[tuple[Student, list[Project]]]: """ Query all students that are causing conflicts for a certain edition Return a ConflictStudent for each student that causes a conflict From 823bc7f30b237ab0562803dfc2be700cd83d3d06 Mon Sep 17 00:00:00 2001 From: Francis Date: Mon, 4 Apr 2022 13:19:25 +0200 Subject: [PATCH 230/536] disable typing in tests --- .github/workflows/backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index b43bb7704..2be992c0f 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -75,4 +75,4 @@ jobs: path: /usr/local/lib/python3.10/site-packages key: venv-${{ runner.os }}-bullseye-3.10.2-v2-${{ hashFiles('**/poetry.lock') }} - - run: python -m mypy src tests + - run: python -m mypy src From b8f5ed81a5cd309022a324ec02383d3635d4c338 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 4 Apr 2022 16:18:26 +0200 Subject: [PATCH 231/536] add poetry to install instructions --- README.md | 24 ++++++++++++++++++++---- backend/README.md | 4 ++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 11aeafa3b..1b8649069 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,9 @@ Below are the instructions on how to set up the frontend and backend. Instructio - Create a `Virtual Environment` (`python3 -m venv venv`) -- Install the dependencies (`pip3 install -r requirements.txt -r requirements-dev.txt`) +- Install `Poetry` (`pip3 install poetry`) + +- Install the dependencies (`poetry install`) - Required scripts: @@ -129,7 +131,7 @@ Below are the instructions on how to set up the frontend and backend. Instructio ```shell # Install the required Node version nvm install 16.14.1 - + # Make your shell use the newly-installed version nvm use 16 ``` @@ -218,10 +220,24 @@ yarn build source venv/bin/activate ``` -3. Install the regular dependencies and development dependencies +3. Install Poetry and configure it to use the `Virtual Environment` we created in the previous step + + ```shell + pip3 install poetry + + # Use the existing venv instead of creating a new one + poetry config virtualenvs.create false + poetry config virtualenvs.in-project true + ``` + +3. Install the dependencies ``` - pip3 install -r requirements.txt -r requirements-dev.txt + # Install all dependencies + poetry install + + # Only install production dependencies + poetry install --no-dev ``` For all commands below, make sure your `Virtual Environment` is activated at all times. Otherwise, your Python interpreter won't be able to find the correct package. diff --git a/backend/README.md b/backend/README.md index 0f7a5a341..1b739db34 100644 --- a/backend/README.md +++ b/backend/README.md @@ -20,6 +20,10 @@ pip3 install poetry # Install all dependencies and dev dependencies poetry install + +# Use the existing venv instead of creating a new one +poetry config virtualenvs.create false +poetry config virtualenvs.in-project true ``` Note that, in case your IDE does not do this automatically, you have to run `source venv/bin/activate` every time you want to run the backend, as otherwise your interpreter won't be able to find the packages. From 51dafbbccdd0d8af8e9098baf7c17c5dcb6b9db0 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Tue, 5 Apr 2022 12:34:48 +0200 Subject: [PATCH 232/536] Change invite button --- .../UsersComponents/InviteUser/InviteUser.tsx | 11 +++++-- .../InviteUserComponents/ButtonsDiv.tsx | 22 ++++++++++---- .../InviteUserComponents/MessageDiv.tsx | 14 +++++++++ .../InviteUser/InviteUserComponents/index.ts | 1 + .../UsersComponents/InviteUser/styles.ts | 30 ++++++------------- frontend/src/utils/api/users/users.ts | 3 +- 6 files changed, 51 insertions(+), 30 deletions(-) create mode 100644 frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/MessageDiv.tsx diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx index 57d83661c..bbdc9c87e 100644 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { getInviteLink } from "../../../utils/api/users/users"; import "./InviteUser.css"; import { InviteInput, InviteContainer } from "./styles"; -import { ButtonsDiv, ErrorDiv } from "./InviteUserComponents"; +import { ButtonsDiv, ErrorDiv, MessageDiv } from "./InviteUserComponents"; /** * A component to invite a user as coach to a given edition. @@ -15,11 +15,13 @@ export default function InviteUser(props: { edition: string }) { 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 const changeEmail = function (email: string) { setEmail(email); setValid(true); setErrorMessage(""); + setMessage(""); }; const sendInvite = async (copyInvite: boolean) => { @@ -28,19 +30,23 @@ export default function InviteUser(props: { edition: string }) { try { const response = await getInviteLink(props.edition, email); if (copyInvite) { - await navigator.clipboard.writeText(response.mailTo); + await navigator.clipboard.writeText(response.inviteLink); + setMessage("Copied invite link for " + email); } else { window.open(response.mailTo); + setMessage("Created mail for " + email); } setLoading(false); setEmail(""); } catch (error) { setLoading(false); setErrorMessage("Something went wrong"); + setMessage(""); } } else { setValid(false); setErrorMessage("Invalid email"); + setMessage(""); } }; @@ -54,6 +60,7 @@ export default function InviteUser(props: { edition: string }) { /> +
                                        ); diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ButtonsDiv.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ButtonsDiv.tsx index 0ad3a1373..d7cf21d70 100644 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ButtonsDiv.tsx +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ButtonsDiv.tsx @@ -1,5 +1,6 @@ -import { CopyButton, InviteButton, Loader } from "../styles"; +import { InviteButton, Loader } from "../styles"; import React from "react"; +import { Button, ButtonGroup, Dropdown } from "react-bootstrap"; /** * A component to choice between sending an invite or copying it to clipboard. @@ -14,9 +15,20 @@ export default function ButtonsDiv(props: { return ; } return ( -
                                        - props.sendInvite(false)}>Send invite - props.sendInvite(true)}>Copy invite -
                                        + + + + + + + + props.sendInvite(true)}> + Copy invite link + + + + ); } diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/MessageDiv.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/MessageDiv.tsx new file mode 100644 index 000000000..46c938c88 --- /dev/null +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/MessageDiv.tsx @@ -0,0 +1,14 @@ +import { Message } from "../styles"; +import React from "react"; + +/** + * A component which shows a message if there is one. + * @param props.message The possible message. + */ +export default function MessageDiv(props: { message: string }) { + let messageDiv = null; + if (props.message) { + messageDiv = {props.message}; + } + return messageDiv; +} diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/index.ts b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/index.ts index 6d540a8e1..df676306a 100644 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/index.ts +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/index.ts @@ -1,2 +1,3 @@ export { default as ButtonsDiv } from "./ButtonsDiv"; export { default as ErrorDiv } from "./ErrorDiv"; +export { default as MessageDiv } from "./MessageDiv"; diff --git a/frontend/src/components/UsersComponents/InviteUser/styles.ts b/frontend/src/components/UsersComponents/InviteUser/styles.ts index 7a70712e0..5b30a046f 100644 --- a/frontend/src/components/UsersComponents/InviteUser/styles.ts +++ b/frontend/src/components/UsersComponents/InviteUser/styles.ts @@ -1,5 +1,4 @@ import styled, { keyframes } from "styled-components"; -import { Button } from "react-bootstrap"; export const InviteContainer = styled.div` clear: both; @@ -14,32 +13,13 @@ export const InviteInput = styled.input.attrs({ font-size: 13px; margin-top: 10px; margin-left: 10px; + margin-right: 5px; text-align: center; border-radius: 5px; border-width: 0; float: left; `; -export const InviteButton = styled(Button).attrs({ - size: "sm", -})` - cursor: pointer; - background: var(--osoc_green); - color: white; - margin-left: 7px; - margin-top: 10px; -`; - -export const CopyButton = styled(Button).attrs({ - size: "sm", -})` - cursor: pointer; - background: var(--osoc_orange); - color: black; - margin-left: 7px; - margin-top: 10px; -`; - const rotate = keyframes` from { transform: rotate(0deg); @@ -66,3 +46,11 @@ export const Error = styled.div` margin-left: 10px; color: var(--osoc_red); `; + +export const Message = styled.div` + margin-left: 10px; +`; + +export const InviteButton = styled.div` + padding-top: 10px; +`; diff --git a/frontend/src/utils/api/users/users.ts b/frontend/src/utils/api/users/users.ts index 736fb94e0..28b95722b 100644 --- a/frontend/src/utils/api/users/users.ts +++ b/frontend/src/utils/api/users/users.ts @@ -19,7 +19,7 @@ export interface UsersList { */ export interface MailTo { mailTo: string; - link: string; + inviteLink: string; } /** @@ -27,7 +27,6 @@ export interface MailTo { */ export async function getInviteLink(edition: string, email: string): Promise { const response = await axiosInstance.post(`/editions/${edition}/invites/`, { email: email }); - console.log(response); return response.data as MailTo; } From 59de815a70c02f9383d3d606c4293207cc68f7fb Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Tue, 5 Apr 2022 13:38:55 +0200 Subject: [PATCH 233/536] delete user from all editions --- backend/src/app/logic/users.py | 8 +++++ backend/src/app/routers/users/users.py | 9 +++++ backend/src/database/crud/users.py | 16 ++++++++- .../test_database/test_crud/test_users.py | 31 ++++++++++++++++ .../test_routers/test_users/test_users.py | 35 +++++++++++++++++++ 5 files changed, 98 insertions(+), 1 deletion(-) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index 5a10f6e70..e9241d66c 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -48,6 +48,14 @@ def remove_coach(db: Session, user_id: int, edition_name: str): users_crud.remove_coach(db, user_id, edition_name) +def remove_coach_all_editions(db: Session, user_id: int): + """ + Remove user as coach from all editions + """ + + users_crud.remove_coach_all_editions(db, user_id) + + def get_request_list(db: Session, edition_name: str | None) -> UserRequestsResponse: """ Query the database for a list of all user requests diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index 6103556c2..26a0623b1 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -46,6 +46,15 @@ async def remove_from_edition(user_id: int, edition_name: str, db: Session = Dep logic.remove_coach(db, user_id, edition_name) +@users_router.delete("/{user_id}/editions", status_code=204, dependencies=[Depends(require_admin)]) +async def remove_from_all_edition(user_id: int, db: Session = Depends(get_session)): + """ + Remove user as coach from all editions + """ + + logic.remove_coach_all_editions(db, user_id) + + @users_router.get("/requests", response_model=UserRequestsResponse, dependencies=[Depends(require_admin)]) async def get_requests(edition: str | None = Query(None), db: Session = Depends(get_session)): """ diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 136593a17..2a105abc2 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -31,7 +31,11 @@ def get_admins_from_edition(db: Session, edition_name: str) -> list[User]: Get all admins from the given edition """ edition = db.query(Edition).where(Edition.name == edition_name).one() - return db.query(User).where(User.admin).join(user_editions).filter(user_editions.c.edition_id == edition.edition_id).all() + return db.query(User)\ + .where(User.admin)\ + .join(user_editions)\ + .filter(user_editions.c.edition_id == edition.edition_id)\ + .all() def edit_admin_status(db: Session, user_id: int, admin: bool): @@ -60,6 +64,7 @@ def remove_coach(db: Session, 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)\ .where(user_editions.c.user_id == user_id)\ @@ -68,6 +73,15 @@ def remove_coach(db: Session, user_id: int, edition_name: str): db.commit() +def remove_coach_all_editions(db: Session, 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() + + def get_all_requests(db: Session) -> list[CoachRequest]: """ Get all userrequests diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index a975a1395..71f11679d 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -145,6 +145,37 @@ def test_remove_coach(database_session: Session): assert len(database_session.query(user_editions).all()) == 1 +def test_remove_coach_all_editions(database_session: Session): + """Test removing a user as coach from all editions""" + + # Create user + user1 = models.User(name="user1", admin=False) + database_session.add(user1) + user2 = models.User(name="user2", admin=False) + database_session.add(user2) + + # Create edition + edition1 = models.Edition(year=1, name="ed1") + edition2 = models.Edition(year=2, name="ed2") + edition3 = models.Edition(year=3, name="ed3") + database_session.add(edition1) + database_session.add(edition2) + database_session.add(edition3) + + database_session.commit() + + # Create coach role + 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 + + def test_get_all_requests(database_session: Session): """Test get request for all userrequests""" diff --git a/backend/tests/test_routers/test_users/test_users.py b/backend/tests/test_routers/test_users/test_users.py index 99d9c8bd9..6c0563fe8 100644 --- a/backend/tests/test_routers/test_users/test_users.py +++ b/backend/tests/test_routers/test_users/test_users.py @@ -164,6 +164,41 @@ def test_remove_coach(database_session: Session, auth_client: AuthClient): assert len(coach) == 0 + +def test_remove_coach_all_editions(database_session: Session, auth_client: AuthClient): + """Test removing a user as coach from all editions""" + auth_client.admin() + + # Create user + user1 = models.User(name="user1", admin=False) + database_session.add(user1) + user2 = models.User(name="user2", admin=False) + database_session.add(user2) + + # Create edition + edition1 = models.Edition(year=1, name="ed1") + edition2 = models.Edition(year=2, name="ed2") + edition3 = models.Edition(year=3, name="ed3") + database_session.add(edition1) + database_session.add(edition2) + database_session.add(edition3) + + database_session.commit() + + # Create coach role + 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 + + def test_get_all_requests(database_session: Session, auth_client: AuthClient): """Test endpoint for getting all userrequests""" auth_client.admin() From 7dd433bec1698fa08d596c75ef4379064ea42e62 Mon Sep 17 00:00:00 2001 From: Seppe <57944475+SeppeM8@users.noreply.github.com> Date: Tue, 5 Apr 2022 13:47:51 +0200 Subject: [PATCH 234/536] Fix typo Co-authored-by: Stijn De Clercq <60451863+stijndcl@users.noreply.github.com> --- backend/src/app/routers/users/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index 26a0623b1..27702f821 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -47,7 +47,7 @@ async def remove_from_edition(user_id: int, edition_name: str, db: Session = Dep @users_router.delete("/{user_id}/editions", status_code=204, dependencies=[Depends(require_admin)]) -async def remove_from_all_edition(user_id: int, db: Session = Depends(get_session)): +async def remove_from_all_editions(user_id: int, db: Session = Depends(get_session)): """ Remove user as coach from all editions """ From 10de3aba49ad390a745e003e0ce73aa1aa14c01c Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Fri, 1 Apr 2022 17:14:28 +0200 Subject: [PATCH 235/536] started working on projects page --- .../ProjectCard/ProjectCard.tsx | 24 +++++++++++++ .../ProjectsComponents/ProjectCard/index.ts | 1 + .../ProjectsComponents/ProjectCard/styles.ts | 36 +++++++++++++++++++ .../components/ProjectsComponents/index.ts | 1 + .../src/views/ProjectsPage/ProjectsPage.tsx | 32 ++++++++++++++++- frontend/src/views/ProjectsPage/styles.ts | 8 +++++ 6 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx create mode 100644 frontend/src/components/ProjectsComponents/ProjectCard/index.ts create mode 100644 frontend/src/components/ProjectsComponents/ProjectCard/styles.ts create mode 100644 frontend/src/components/ProjectsComponents/index.ts create mode 100644 frontend/src/views/ProjectsPage/styles.ts diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx new file mode 100644 index 000000000..d0d5e1231 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -0,0 +1,24 @@ +import { CardContainer, CoachesContainer, CoachContainer, Delete } from "./styles"; + +export default function ProjectCard({ + name, + client, + coaches, +}: { + name: string; + client: string; + coaches: string[]; +}) { + return ( + +

                                        {name}

                                        +

                                        {client}

                                        + + {coaches.map((element, index) => ( + {element} + ))} + + X +
                                        + ); +} diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/index.ts b/frontend/src/components/ProjectsComponents/ProjectCard/index.ts new file mode 100644 index 000000000..b45666a95 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/ProjectCard/index.ts @@ -0,0 +1 @@ +export { default } from "./ProjectCard"; diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts new file mode 100644 index 000000000..bcbe2fdba --- /dev/null +++ b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts @@ -0,0 +1,36 @@ +import styled from "styled-components"; + +export const CardContainer = styled.div` + border: 2px solid #1a1a36; + border-radius: 20px; + margin: 20px; + margin-bottom: 5px; + padding: 20px 50px 20px 20px; + background-color: #323252; + box-shadow: 5px 5px 15px #131329; +`; + +export const CoachesContainer = styled.div` + display: flex; + margin-top: 20px; +`; + +export const CoachContainer = styled.div` + background-color: #1a1a36; + border-radius: 10px; + margin-right: 10px; + text-align: center; + padding: 10px; + max-width: 50%; + text-overflow: ellipsis; + overflow: hidden; +`; + +export const Delete = styled.button` + background-color: #f14a3b; + padding: 5px 10px; + border: 0; + border-radius: 5px; + margin-top: 20px; + margin-left: 100%; +`; diff --git a/frontend/src/components/ProjectsComponents/index.ts b/frontend/src/components/ProjectsComponents/index.ts new file mode 100644 index 000000000..09548c092 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/index.ts @@ -0,0 +1 @@ +export { default as ProjectCard } from "./ProjectCard"; diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index d6469892f..4b6f31773 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -1,8 +1,38 @@ import React from "react"; import "./ProjectsPage.css"; +import { ProjectCard } from "../../components/ProjectsComponents"; + +import { CardsGrid } from "./styles"; + function ProjectPage() { - return
                                        This is the projects page
                                        ; + return ( +
                                        + + + + + + + + + + + +
                                        + ); } export default ProjectPage; diff --git a/frontend/src/views/ProjectsPage/styles.ts b/frontend/src/views/ProjectsPage/styles.ts new file mode 100644 index 000000000..3c93d10af --- /dev/null +++ b/frontend/src/views/ProjectsPage/styles.ts @@ -0,0 +1,8 @@ +import styled from "styled-components"; + +export const CardsGrid = styled.div` + display: grid; + grid-gap: 5px; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-auto-flow: dense; +`; From 480a9e5f9a68c8fdee16ee372632005e59cfe06f Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 2 Apr 2022 13:15:01 +0200 Subject: [PATCH 236/536] moved delete button to top right api call for get projects --- .../ProjectCard/ProjectCard.tsx | 12 +++++++++--- .../ProjectsComponents/ProjectCard/styles.ts | 12 +++++++++--- frontend/src/utils/api/projects.ts | 18 ++++++++++++++++++ .../src/views/ProjectsPage/ProjectsPage.tsx | 18 +++++++++++++++++- 4 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 frontend/src/utils/api/projects.ts diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index d0d5e1231..9e5fae533 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -1,4 +1,4 @@ -import { CardContainer, CoachesContainer, CoachContainer, Delete } from "./styles"; +import { CardContainer, CoachesContainer, CoachContainer, Delete, TitleContainer } from "./styles"; export default function ProjectCard({ name, @@ -11,14 +11,20 @@ export default function ProjectCard({ }) { return ( -

                                        {name}

                                        + +
                                        +

                                        {name}

                                        +
                                        + + X +
                                        +

                                        {client}

                                        {coaches.map((element, index) => ( {element} ))} - X
                                        ); } diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts index bcbe2fdba..86bf7cc25 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts +++ b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts @@ -5,11 +5,16 @@ export const CardContainer = styled.div` border-radius: 20px; margin: 20px; margin-bottom: 5px; - padding: 20px 50px 20px 20px; + padding: 20px 20px 20px 20px; background-color: #323252; box-shadow: 5px 5px 15px #131329; `; +export const TitleContainer = styled.div` + display: flex; + justify-content: space-between; +`; + export const CoachesContainer = styled.div` display: flex; margin-top: 20px; @@ -22,6 +27,7 @@ export const CoachContainer = styled.div` text-align: center; padding: 10px; max-width: 50%; + min-width: 20%; text-overflow: ellipsis; overflow: hidden; `; @@ -31,6 +37,6 @@ export const Delete = styled.button` padding: 5px 10px; border: 0; border-radius: 5px; - margin-top: 20px; - margin-left: 100%; + max-height: 35px; + margin-left: 5%; `; diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts new file mode 100644 index 000000000..bf9ce358a --- /dev/null +++ b/frontend/src/utils/api/projects.ts @@ -0,0 +1,18 @@ +import axios from "axios"; +import { axiosInstance } from "./api"; + +export async function getProjects(edition: string) { + try { + const response = await axiosInstance.get("/editions/" + edition + "/projects"); + console.log(response); + + const projects = response.data; + return projects; + } catch (error) { + if (axios.isAxiosError(error)) { + return false; + } else { + throw error; + } + } +} diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index 4b6f31773..64287da75 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -1,4 +1,5 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; +import { getProjects } from "../../utils/api/projects"; import "./ProjectsPage.css"; import { ProjectCard } from "../../components/ProjectsComponents"; @@ -6,6 +7,21 @@ import { ProjectCard } from "../../components/ProjectsComponents"; import { CardsGrid } from "./styles"; function ProjectPage() { + const [projects, setProjects] = useState(); + + useEffect(() => { + async function callProjects() { + const response = await getProjects("1"); + if (response) { + console.log(response); + setProjects(response); + } + } + if (!projects) { + callProjects(); + } else console.log("hello"); + }); + return (
                                        From 1e40a36f4336eb58fff360b4fb36492e8d049765 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 3 Apr 2022 12:11:32 +0200 Subject: [PATCH 237/536] show projectscard returned from api call --- .../ProjectCard/ProjectCard.tsx | 2 +- frontend/src/utils/api/projects.ts | 4 +-- .../src/views/ProjectsPage/ProjectsPage.tsx | 25 +++++++++++++++---- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 9e5fae533..826ad7ef2 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -21,7 +21,7 @@ export default function ProjectCard({

                                        {client}

                                        - {coaches.map((element, index) => ( + {coaches.map((element, _index) => ( {element} ))} diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index bf9ce358a..5c4c6982b 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -3,9 +3,7 @@ import { axiosInstance } from "./api"; export async function getProjects(edition: string) { try { - const response = await axiosInstance.get("/editions/" + edition + "/projects"); - console.log(response); - + const response = await axiosInstance.get("/editions/" + edition + "/projects"); const projects = response.data; return projects; } catch (error) { diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index 64287da75..0cdd6ae91 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -6,25 +6,40 @@ import { ProjectCard } from "../../components/ProjectsComponents"; import { CardsGrid } from "./styles"; +interface Project { + name: string; + partners: any[]; +} + function ProjectPage() { - const [projects, setProjects] = useState(); + const [projects, setProjects] = useState>([]); + const [gotProjects, setGotProjects] = useState(false); useEffect(() => { async function callProjects() { const response = await getProjects("1"); if (response) { - console.log(response); - setProjects(response); + setGotProjects(true); + setProjects(response.projects); } } - if (!projects) { + if (!gotProjects) { callProjects(); - } else console.log("hello"); + } }); return (
                                        + {projects.map((project, _index) => ( + + ))} + Date: Sun, 3 Apr 2022 12:59:14 +0200 Subject: [PATCH 238/536] fixed key property --- .../ProjectsComponents/ProjectCard/ProjectCard.tsx | 2 +- frontend/src/views/ProjectsPage/ProjectsPage.tsx | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 826ad7ef2..4772f57dc 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -22,7 +22,7 @@ export default function ProjectCard({

                                        {client}

                                        {coaches.map((element, _index) => ( - {element} + {element} ))} diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index 0cdd6ae91..e084fa9d5 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -33,10 +33,10 @@ function ProjectPage() { {projects.map((project, _index) => ( ))} @@ -50,17 +50,11 @@ function ProjectPage() { client="client 2" coaches={["Miet", "Bart", "Dirk de lange", "Jef de korte"]} /> - - - - - -
                                        ); From b012f20d45c8e1560aac9530fd5baef5bf6965cf Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 3 Apr 2022 13:26:40 +0200 Subject: [PATCH 239/536] placeholders for header --- frontend/src/views/ProjectsPage/ProjectsPage.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index e084fa9d5..69d663440 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -30,6 +30,12 @@ function ProjectPage() { return (
                                        +
                                        + + + +
                                        + {projects.map((project, _index) => ( ))} - Date: Wed, 6 Apr 2022 11:14:11 +0200 Subject: [PATCH 240/536] Fix TODO's and refactor coaches list --- backend/src/database/crud/users.py | 2 + .../UsersComponents/Coaches/Coaches.tsx | 71 ++++++++----------- .../Coaches/CoachesComponents/CoachList.tsx | 4 +- .../CoachesComponents/CoachListItem.tsx | 5 +- .../UsersComponents/Coaches/styles.ts | 10 +++ .../PendingRequests/PendingRequests.tsx | 16 ++++- .../AcceptReject.tsx | 20 +++++- .../PendingRequestsComponents/RequestList.tsx | 8 ++- .../RequestListItem.tsx | 13 ++-- .../UsersComponents/PendingRequests/styles.ts | 14 +++- frontend/src/utils/api/users/admins.ts | 6 +- frontend/src/utils/api/users/coaches.ts | 4 +- frontend/src/utils/api/users/requests.ts | 8 ++- frontend/src/views/UsersPage/UsersPage.tsx | 53 +++++++++++++- 14 files changed, 164 insertions(+), 70 deletions(-) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index bc8ffa66a..88d939448 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -112,6 +112,7 @@ def accept_request(db: Session, request_id: int): 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() def reject_request(db: Session, request_id: int): @@ -120,3 +121,4 @@ def reject_request(db: Session, request_id: int): """ db.query(CoachRequest).where(CoachRequest.request_id == request_id).delete() + db.commit() diff --git a/frontend/src/components/UsersComponents/Coaches/Coaches.tsx b/frontend/src/components/UsersComponents/Coaches/Coaches.tsx index c5adb1a70..bc02a7519 100644 --- a/frontend/src/components/UsersComponents/Coaches/Coaches.tsx +++ b/frontend/src/components/UsersComponents/Coaches/Coaches.tsx @@ -1,54 +1,41 @@ import React, { useEffect, useState } from "react"; import { CoachesTitle, CoachesContainer } from "./styles"; -import { getUsers, User } from "../../../utils/api/users/users"; +import { User } from "../../../utils/api/users/users"; import { Error, SearchInput } from "../PendingRequests/styles"; -import { getCoaches } from "../../../utils/api/users/coaches"; import { CoachList, AddCoach } from "./CoachesComponents"; /** * 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 need to be shown. + * @param props.allCoaches The list of all coaches of the current edition. + * @param props.users A list of all users who can be added as coach. + * @param props.refresh A function which will be called when a coach is added/removed. + * @param props.gotData All data is received. + * @param props.gettingData Data is not available yet. + * @param props.error An error message. */ -export default function Coaches(props: { edition: string }) { - const [allCoaches, setAllCoaches] = useState([]); // All coaches from the edition +export default function Coaches(props: { + edition: string; + allCoaches: User[]; + users: User[]; + refresh: () => void; + gotData: boolean; + gettingData: boolean; + error: string; +}) { const [coaches, setCoaches] = useState([]); // All coaches after filter - const [users, setUsers] = useState([]); // All users which are not a coach - const [gettingData, setGettingData] = useState(false); // Waiting for data const [searchTerm, setSearchTerm] = useState(""); // The word set in filter - const [gotData, setGotData] = useState(false); // Received data - const [error, setError] = useState(""); // Error message - - async function getData() { - setGettingData(true); - setGotData(false); - try { - const coachResponse = await getCoaches(props.edition); - setAllCoaches(coachResponse.users); - setCoaches(coachResponse.users); - - const UsersResponse = await getUsers(); - const users = []; - for (const user of UsersResponse.users) { - if (!coachResponse.users.some(e => e.userId === user.userId)) { - users.push(user); - } - } - setUsers(users); - - setGotData(true); - setGettingData(false); - } catch (exception) { - setError("Oops, something went wrong..."); - setGettingData(false); - } - } useEffect(() => { - if (!gotData && !gettingData && !error) { - getData(); + const newCoaches: User[] = []; + for (const coach of props.allCoaches) { + if (coach.name.toUpperCase().includes(searchTerm.toUpperCase())) { + newCoaches.push(coach); + } } - }, [gotData, gettingData, error, getData]); + setCoaches(newCoaches); + }, [props.allCoaches, searchTerm]); /** * Apply a filter to the coach list. @@ -58,7 +45,7 @@ export default function Coaches(props: { edition: string }) { const filter = (word: string) => { setSearchTerm(word); const newCoaches: User[] = []; - for (const coach of allCoaches) { + for (const coach of props.allCoaches) { if (coach.name.toUpperCase().includes(word.toUpperCase())) { newCoaches.push(coach); } @@ -70,15 +57,15 @@ export default function Coaches(props: { edition: string }) { Coaches filter(e.target.value)} /> - + - {error} + {props.error} ); } diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx index 476862e2f..3e1dc738b 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx @@ -1,7 +1,7 @@ import { User } from "../../../../utils/api/users/users"; import { SpinnerContainer } from "../../PendingRequests/styles"; import { Spinner } from "react-bootstrap"; -import { CoachesTable } from "../styles"; +import { CoachesTable, RemoveTh } from "../styles"; import React from "react"; import { CoachListItem } from "./index"; @@ -53,7 +53,7 @@ export default function CoachList(props: { Name Email - Remove from edition + Remove from edition {body} diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx index e00703c0f..821e9ff56 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx @@ -1,6 +1,7 @@ import { User } from "../../../../utils/api/users/users"; import React from "react"; import RemoveCoach from "./RemoveCoach"; +import { RemoveTd } from "../styles"; /** * An item from [[CoachList]] which represents one coach. @@ -18,9 +19,9 @@ export default function CoachListItem(props: { {props.coach.name} {props.coach.email} - + - + ); } diff --git a/frontend/src/components/UsersComponents/Coaches/styles.ts b/frontend/src/components/UsersComponents/Coaches/styles.ts index 1d388b8f1..5c29a2ac5 100644 --- a/frontend/src/components/UsersComponents/Coaches/styles.ts +++ b/frontend/src/components/UsersComponents/Coaches/styles.ts @@ -20,3 +20,13 @@ 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 RemoveTd = styled.td` + text-align: center; + vertical-align: middle; +`; diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx index 22dfe8aa6..4d2b02375 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx @@ -9,7 +9,7 @@ import { RequestFilter, RequestList, RequestsHeader } from "./PendingRequestsCom * Every request can be accepted or rejected. * @param props.edition The edition. */ -export default function PendingRequests(props: { edition: string }) { +export default function PendingRequests(props: { edition: string; refreshCoaches: () => void }) { const [allRequests, setAllRequests] = useState([]); // All requests for the given edition const [requests, setRequests] = useState([]); // All requests after filter const [gettingRequests, setGettingRequests] = useState(false); // Waiting for data @@ -18,6 +18,13 @@ export default function PendingRequests(props: { edition: string }) { const [open, setOpen] = useState(false); // Collapsible is open const [error, setError] = useState(""); // Error message + function refresh(coachAdded: boolean) { + getData(); + if (coachAdded) { + props.refreshCoaches(); + } + } + async function getData() { try { const response = await getRequests(props.edition); @@ -61,7 +68,12 @@ export default function PendingRequests(props: { edition: string }) { filter={word => filter(word)} show={allRequests.length > 0} /> - + {error} diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/AcceptReject.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/AcceptReject.tsx index 823697161..7d678a895 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/AcceptReject.tsx +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/AcceptReject.tsx @@ -5,12 +5,26 @@ import React from "react"; /** * Component consisting of two buttons to accept or reject a coach request. * @param props.requestId The id of the request. + * @param props.refresh A function which will be called when a request is accepted/rejected. */ -export default function AcceptReject(props: { requestId: number }) { +export default function AcceptReject(props: { + requestId: number; + refresh: (coachAdded: boolean) => void; +}) { + async function accept() { + await acceptRequest(props.requestId); + props.refresh(true); + } + + async function reject() { + await rejectRequest(props.requestId); + props.refresh(false); + } + return (
                                        - acceptRequest(props.requestId)}>Accept - rejectRequest(props.requestId)}>Reject + Accept + Reject
                                        ); } diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx index 17589f8ed..a1fe80f20 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx @@ -9,11 +9,13 @@ import RequestListItem from "./RequestListItem"; * @param props.requests A list of requests which need to be shown. * @param props.loading Waiting for data. * @param props.gotData Data is received. + * @param props.refresh A function which will be called when a request is accepted/rejected. */ export default function RequestList(props: { requests: Request[]; loading: boolean; gotData: boolean; + refresh: (coachAdded: boolean) => void; }) { if (props.loading) { return ( @@ -32,7 +34,11 @@ export default function RequestList(props: { const body = ( {props.requests.map(request => ( - + ))} ); diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestListItem.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestListItem.tsx index 820476dbc..ba0caf9c4 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestListItem.tsx +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestListItem.tsx @@ -1,20 +1,25 @@ import { Request } from "../../../../utils/api/users/requests"; import React from "react"; import AcceptReject from "./AcceptReject"; +import { AcceptRejectTd } from "../styles"; /** * An item from [[RequestList]] which represents one request. * This includes two buttons to accept and reject the request. * @param props.request The request which is represented. + * @param props.refresh A function which will be called when a request is accepted/rejected. */ -export default function RequestListItem(props: { request: Request }) { +export default function RequestListItem(props: { + request: Request; + refresh: (coachAdded: boolean) => void; +}) { return ( {props.request.user.name} {props.request.user.email} - - - + + + ); } diff --git a/frontend/src/components/UsersComponents/PendingRequests/styles.ts b/frontend/src/components/UsersComponents/PendingRequests/styles.ts index 03be79ef6..eda10d0ca 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/styles.ts +++ b/frontend/src/components/UsersComponents/PendingRequests/styles.ts @@ -17,14 +17,14 @@ export const RequestHeaderTitle = styled.div` export const OpenArrow = styled(BiDownArrow)` margin-top: 13px; margin-left: 10px; - offset-position: 0px 30px; + offset-position: 0 30px; `; export const ClosedArrow = styled(BiDownArrow)` margin-top: 13px; margin-left: 10px; transform: rotate(-90deg); - offset: 0px 30px; + offset: 0 30px; `; export const SearchInput = styled.input.attrs({ @@ -46,7 +46,13 @@ export const PendingRequestsContainer = styled.div` `; export const AcceptRejectTh = styled.th` - width: 150px; + width: 200px; + text-align: center; +`; + +export const AcceptRejectTd = styled.td` + text-align: center; + vertical-align: middle; `; export const AcceptButton = styled(Button)` @@ -77,4 +83,6 @@ export const SpinnerContainer = styled.div` export const Error = styled.div` color: var(--osoc_red); + width: 100%; + margin: auto; `; diff --git a/frontend/src/utils/api/users/admins.ts b/frontend/src/utils/api/users/admins.ts index e7202af97..695db192e 100644 --- a/frontend/src/utils/api/users/admins.ts +++ b/frontend/src/utils/api/users/admins.ts @@ -32,7 +32,7 @@ export async function removeAdmin(userId: number) { * @param {number} userId The id of the user */ export async function removeAdminAndCoach(userId: number) { - const response = await axiosInstance.patch(`/users/${userId}`, { admin: false }); - // TODO: remove user from all editions - return response.status === 204; + const response1 = await axiosInstance.patch(`/users/${userId}`, { admin: false }); + const response2 = await axiosInstance.delete(`/users/${userId}/editions`); + return response1.status === 204 && response2.status === 204; } diff --git a/frontend/src/utils/api/users/coaches.ts b/frontend/src/utils/api/users/coaches.ts index 8f7e03a5f..1a56a47dd 100644 --- a/frontend/src/utils/api/users/coaches.ts +++ b/frontend/src/utils/api/users/coaches.ts @@ -25,8 +25,8 @@ export async function removeCoachFromEdition(userId: number, edition: string): P * @param {number} userId The user's id */ export async function removeCoachFromAllEditions(userId: number): Promise { - // TODO: sent correct DELETE - return false; + const response = await axiosInstance.delete(`/users/${userId}/editions`); + return response.status === 204; } /** diff --git a/frontend/src/utils/api/users/requests.ts b/frontend/src/utils/api/users/requests.ts index 055facbc5..c0225b36e 100644 --- a/frontend/src/utils/api/users/requests.ts +++ b/frontend/src/utils/api/users/requests.ts @@ -29,8 +29,9 @@ export async function getRequests(edition: string): Promise * Accept a coach request * @param {number} requestId The id of the request */ -export async function acceptRequest(requestId: number) { - alert("Accept " + requestId); +export async function acceptRequest(requestId: number): Promise { + const response = await axiosInstance.post(`/users/requests/${requestId}/accept`); + return response.status === 204; } /** @@ -38,5 +39,6 @@ export async function acceptRequest(requestId: number) { * @param {number} requestId The id of the request */ export async function rejectRequest(requestId: number) { - alert("Reject " + requestId); + const response = await axiosInstance.post(`/users/requests/${requestId}/reject`); + return response.status === 204; } diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index 0ef288f2e..3d15dc8b1 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -1,17 +1,56 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { UsersPageDiv, AdminsButton, UsersHeader } from "./styles"; import { Coaches } from "../../components/UsersComponents/Coaches"; import { InviteUser } from "../../components/UsersComponents/InviteUser"; import { PendingRequests } from "../../components/UsersComponents/PendingRequests"; +import { getUsers, User } from "../../utils/api/users/users"; +import { getCoaches } from "../../utils/api/users/coaches"; /** * Page for admins to manage coach and admin settings. */ function UsersPage() { + const [allCoaches, setAllCoaches] = useState([]); // All coaches from the edition + const [users, setUsers] = useState([]); // All users which are not a coach + const [gettingData, setGettingData] = useState(false); // Waiting for data + const [gotData, setGotData] = useState(false); // Received data + const [error, setError] = useState(""); // Error message + const params = useParams(); const navigate = useNavigate(); + async function getCoachesData() { + setGettingData(true); + setGotData(false); + setError(""); + try { + const coachResponse = await getCoaches(params.edition as string); + setAllCoaches(coachResponse.users); + + const UsersResponse = await getUsers(); + const users = []; + for (const user of UsersResponse.users) { + if (!coachResponse.users.some(e => e.userId === user.userId)) { + users.push(user); + } + } + setUsers(users); + + setGotData(true); + setGettingData(false); + } catch (exception) { + setError("Oops, something went wrong..."); + setGettingData(false); + } + } + + useEffect(() => { + if (!gotData && !gettingData && !error && params.edition !== undefined) { + getCoachesData(); + } + }, [gotData, gettingData, error, getCoachesData, params.edition]); + if (params.edition === undefined) { return
                                        Error
                                        ; } else { @@ -24,8 +63,16 @@ function UsersPage() { navigate("/admins")}>Edit Admins
                                        - - + + ); } From 4a56c6fd67553288527d87d598e6f9fa5c1aa1f0 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Wed, 6 Apr 2022 11:14:12 +0200 Subject: [PATCH 241/536] remove docs folder --- frontend/docs/.nojekyll | 1 - frontend/docs/assets/highlight.css | 85 - frontend/docs/assets/icons.css | 1043 ------------ frontend/docs/assets/icons.png | Bin 9615 -> 0 bytes frontend/docs/assets/icons@2x.png | Bin 28144 -> 0 bytes frontend/docs/assets/main.js | 52 - frontend/docs/assets/search.js | 1 - frontend/docs/assets/style.css | 1413 ----------------- frontend/docs/assets/widgets.png | Bin 480 -> 0 bytes frontend/docs/assets/widgets@2x.png | Bin 855 -> 0 bytes frontend/docs/enums/data.Enums.Role.html | 4 - .../docs/enums/data.Enums.StorageKey.html | 5 - frontend/docs/index.html | 131 -- .../interfaces/contexts.AuthContextState.html | 3 - .../docs/interfaces/data.Interfaces.User.html | 5 - frontend/docs/modules.html | 1 - frontend/docs/modules/App.html | 4 - frontend/docs/modules/Router.html | 4 - .../modules/components.LoginComponents.html | 9 - .../components.RegisterComponents.html | 15 - frontend/docs/modules/components.html | 26 - frontend/docs/modules/contexts.html | 6 - frontend/docs/modules/data.Enums.html | 1 - frontend/docs/modules/data.Interfaces.html | 1 - frontend/docs/modules/data.html | 1 - frontend/docs/modules/utils.Api.html | 7 - frontend/docs/modules/utils.LocalStorage.html | 6 - frontend/docs/modules/utils.html | 1 - frontend/docs/modules/views.Errors.html | 7 - frontend/docs/modules/views.html | 12 - 30 files changed, 2844 deletions(-) delete mode 100644 frontend/docs/.nojekyll delete mode 100644 frontend/docs/assets/highlight.css delete mode 100644 frontend/docs/assets/icons.css delete mode 100644 frontend/docs/assets/icons.png delete mode 100644 frontend/docs/assets/icons@2x.png delete mode 100644 frontend/docs/assets/main.js delete mode 100644 frontend/docs/assets/search.js delete mode 100644 frontend/docs/assets/style.css delete mode 100644 frontend/docs/assets/widgets.png delete mode 100644 frontend/docs/assets/widgets@2x.png delete mode 100644 frontend/docs/enums/data.Enums.Role.html delete mode 100644 frontend/docs/enums/data.Enums.StorageKey.html delete mode 100644 frontend/docs/index.html delete mode 100644 frontend/docs/interfaces/contexts.AuthContextState.html delete mode 100644 frontend/docs/interfaces/data.Interfaces.User.html delete mode 100644 frontend/docs/modules.html delete mode 100644 frontend/docs/modules/App.html delete mode 100644 frontend/docs/modules/Router.html delete mode 100644 frontend/docs/modules/components.LoginComponents.html delete mode 100644 frontend/docs/modules/components.RegisterComponents.html delete mode 100644 frontend/docs/modules/components.html delete mode 100644 frontend/docs/modules/contexts.html delete mode 100644 frontend/docs/modules/data.Enums.html delete mode 100644 frontend/docs/modules/data.Interfaces.html delete mode 100644 frontend/docs/modules/data.html delete mode 100644 frontend/docs/modules/utils.Api.html delete mode 100644 frontend/docs/modules/utils.LocalStorage.html delete mode 100644 frontend/docs/modules/utils.html delete mode 100644 frontend/docs/modules/views.Errors.html delete mode 100644 frontend/docs/modules/views.html diff --git a/frontend/docs/.nojekyll b/frontend/docs/.nojekyll deleted file mode 100644 index e2ac6616a..000000000 --- a/frontend/docs/.nojekyll +++ /dev/null @@ -1 +0,0 @@ -TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. \ No newline at end of file diff --git a/frontend/docs/assets/highlight.css b/frontend/docs/assets/highlight.css deleted file mode 100644 index aea30d60d..000000000 --- a/frontend/docs/assets/highlight.css +++ /dev/null @@ -1,85 +0,0 @@ -:root { - --light-hl-0: #000000; - --dark-hl-0: #D4D4D4; - --light-hl-1: #008000; - --dark-hl-1: #6A9955; - --light-hl-2: #AF00DB; - --dark-hl-2: #C586C0; - --light-hl-3: #0000FF; - --dark-hl-3: #569CD6; - --light-hl-4: #0070C1; - --dark-hl-4: #4FC1FF; - --light-hl-5: #098658; - --dark-hl-5: #B5CEA8; - --light-hl-6: #795E26; - --dark-hl-6: #DCDCAA; - --light-hl-7: #001080; - --dark-hl-7: #9CDCFE; - --light-hl-8: #A31515; - --dark-hl-8: #CE9178; - --light-code-background: #F5F5F5; - --dark-code-background: #1E1E1E; -} - -@media (prefers-color-scheme: light) { :root { - --hl-0: var(--light-hl-0); - --hl-1: var(--light-hl-1); - --hl-2: var(--light-hl-2); - --hl-3: var(--light-hl-3); - --hl-4: var(--light-hl-4); - --hl-5: var(--light-hl-5); - --hl-6: var(--light-hl-6); - --hl-7: var(--light-hl-7); - --hl-8: var(--light-hl-8); - --code-background: var(--light-code-background); -} } - -@media (prefers-color-scheme: dark) { :root { - --hl-0: var(--dark-hl-0); - --hl-1: var(--dark-hl-1); - --hl-2: var(--dark-hl-2); - --hl-3: var(--dark-hl-3); - --hl-4: var(--dark-hl-4); - --hl-5: var(--dark-hl-5); - --hl-6: var(--dark-hl-6); - --hl-7: var(--dark-hl-7); - --hl-8: var(--dark-hl-8); - --code-background: var(--dark-code-background); -} } - -body.light { - --hl-0: var(--light-hl-0); - --hl-1: var(--light-hl-1); - --hl-2: var(--light-hl-2); - --hl-3: var(--light-hl-3); - --hl-4: var(--light-hl-4); - --hl-5: var(--light-hl-5); - --hl-6: var(--light-hl-6); - --hl-7: var(--light-hl-7); - --hl-8: var(--light-hl-8); - --code-background: var(--light-code-background); -} - -body.dark { - --hl-0: var(--dark-hl-0); - --hl-1: var(--dark-hl-1); - --hl-2: var(--dark-hl-2); - --hl-3: var(--dark-hl-3); - --hl-4: var(--dark-hl-4); - --hl-5: var(--dark-hl-5); - --hl-6: var(--dark-hl-6); - --hl-7: var(--dark-hl-7); - --hl-8: var(--dark-hl-8); - --code-background: var(--dark-code-background); -} - -.hl-0 { color: var(--hl-0); } -.hl-1 { color: var(--hl-1); } -.hl-2 { color: var(--hl-2); } -.hl-3 { color: var(--hl-3); } -.hl-4 { color: var(--hl-4); } -.hl-5 { color: var(--hl-5); } -.hl-6 { color: var(--hl-6); } -.hl-7 { color: var(--hl-7); } -.hl-8 { color: var(--hl-8); } -pre, code { background: var(--code-background); } diff --git a/frontend/docs/assets/icons.css b/frontend/docs/assets/icons.css deleted file mode 100644 index 776a3562d..000000000 --- a/frontend/docs/assets/icons.css +++ /dev/null @@ -1,1043 +0,0 @@ -.tsd-kind-icon { - display: block; - position: relative; - padding-left: 20px; - text-indent: -20px; -} -.tsd-kind-icon:before { - content: ""; - display: inline-block; - vertical-align: middle; - width: 17px; - height: 17px; - margin: 0 3px 2px 0; - background-image: url(./icons.png); -} -@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { - .tsd-kind-icon:before { - background-image: url(./icons@2x.png); - background-size: 238px 204px; - } -} - -.tsd-signature.tsd-kind-icon:before { - background-position: 0 -153px; -} - -.tsd-kind-object-literal > .tsd-kind-icon:before { - background-position: 0px -17px; -} -.tsd-kind-object-literal.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -17px; -} -.tsd-kind-object-literal.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -17px; -} - -.tsd-kind-class > .tsd-kind-icon:before { - background-position: 0px -34px; -} -.tsd-kind-class.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -34px; -} -.tsd-kind-class.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -34px; -} - -.tsd-kind-class.tsd-has-type-parameter > .tsd-kind-icon:before { - background-position: 0px -51px; -} -.tsd-kind-class.tsd-has-type-parameter.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -17px -51px; -} -.tsd-kind-class.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -51px; -} - -.tsd-kind-interface > .tsd-kind-icon:before { - background-position: 0px -68px; -} -.tsd-kind-interface.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -68px; -} -.tsd-kind-interface.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -68px; -} - -.tsd-kind-interface.tsd-has-type-parameter > .tsd-kind-icon:before { - background-position: 0px -85px; -} -.tsd-kind-interface.tsd-has-type-parameter.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -17px -85px; -} -.tsd-kind-interface.tsd-has-type-parameter.tsd-is-private - > .tsd-kind-icon:before { - background-position: -34px -85px; -} - -.tsd-kind-namespace > .tsd-kind-icon:before { - background-position: 0px -102px; -} -.tsd-kind-namespace.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -102px; -} -.tsd-kind-namespace.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -102px; -} - -.tsd-kind-module > .tsd-kind-icon:before { - background-position: 0px -102px; -} -.tsd-kind-module.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -102px; -} -.tsd-kind-module.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -102px; -} - -.tsd-kind-enum > .tsd-kind-icon:before { - background-position: 0px -119px; -} -.tsd-kind-enum.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -119px; -} -.tsd-kind-enum.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -119px; -} - -.tsd-kind-enum-member > .tsd-kind-icon:before { - background-position: 0px -136px; -} -.tsd-kind-enum-member.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -136px; -} -.tsd-kind-enum-member.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -136px; -} - -.tsd-kind-signature > .tsd-kind-icon:before { - background-position: 0px -153px; -} -.tsd-kind-signature.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -153px; -} -.tsd-kind-signature.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -153px; -} - -.tsd-kind-type-alias > .tsd-kind-icon:before { - background-position: 0px -170px; -} -.tsd-kind-type-alias.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -170px; -} -.tsd-kind-type-alias.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -170px; -} - -.tsd-kind-type-alias.tsd-has-type-parameter > .tsd-kind-icon:before { - background-position: 0px -187px; -} -.tsd-kind-type-alias.tsd-has-type-parameter.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -17px -187px; -} -.tsd-kind-type-alias.tsd-has-type-parameter.tsd-is-private - > .tsd-kind-icon:before { - background-position: -34px -187px; -} - -.tsd-kind-variable > .tsd-kind-icon:before { - background-position: -136px -0px; -} -.tsd-kind-variable.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -0px; -} -.tsd-kind-variable.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -0px; -} -.tsd-kind-variable.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -0px; -} -.tsd-kind-variable.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -0px; -} -.tsd-kind-variable.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -0px; -} -.tsd-kind-variable.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -0px; -} -.tsd-kind-variable.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -0px; -} -.tsd-kind-variable.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -0px; -} -.tsd-kind-variable.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -0px; -} -.tsd-kind-variable.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -0px; -} -.tsd-kind-variable.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -0px; -} -.tsd-kind-variable.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -0px; -} - -.tsd-kind-property > .tsd-kind-icon:before { - background-position: -136px -0px; -} -.tsd-kind-property.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -0px; -} -.tsd-kind-property.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -0px; -} -.tsd-kind-property.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -0px; -} -.tsd-kind-property.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -0px; -} -.tsd-kind-property.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -0px; -} -.tsd-kind-property.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -0px; -} -.tsd-kind-property.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -0px; -} -.tsd-kind-property.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -0px; -} -.tsd-kind-property.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -0px; -} -.tsd-kind-property.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -0px; -} -.tsd-kind-property.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -0px; -} -.tsd-kind-property.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -0px; -} - -.tsd-kind-get-signature > .tsd-kind-icon:before { - background-position: -136px -17px; -} -.tsd-kind-get-signature.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -17px; -} -.tsd-kind-get-signature.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -17px; -} - -.tsd-kind-set-signature > .tsd-kind-icon:before { - background-position: -136px -34px; -} -.tsd-kind-set-signature.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -34px; -} -.tsd-kind-set-signature.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -34px; -} - -.tsd-kind-accessor > .tsd-kind-icon:before { - background-position: -136px -51px; -} -.tsd-kind-accessor.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -51px; -} -.tsd-kind-accessor.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -51px; -} - -.tsd-kind-function > .tsd-kind-icon:before { - background-position: -136px -68px; -} -.tsd-kind-function.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -68px; -} -.tsd-kind-function.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-function.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -68px; -} -.tsd-kind-function.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -68px; -} -.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -68px; -} -.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -68px; -} -.tsd-kind-function.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-function.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -68px; -} -.tsd-kind-function.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -68px; -} -.tsd-kind-function.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-function.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -68px; -} -.tsd-kind-function.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -68px; -} - -.tsd-kind-method > .tsd-kind-icon:before { - background-position: -136px -68px; -} -.tsd-kind-method.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -68px; -} -.tsd-kind-method.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-method.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -68px; -} -.tsd-kind-method.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -68px; -} -.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -68px; -} -.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -68px; -} -.tsd-kind-method.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-method.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -68px; -} -.tsd-kind-method.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { - background-position: -187px -68px; -} -.tsd-kind-method.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-method.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -68px; -} -.tsd-kind-method.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -68px; -} - -.tsd-kind-call-signature > .tsd-kind-icon:before { - background-position: -136px -68px; -} -.tsd-kind-call-signature.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -68px; -} -.tsd-kind-call-signature.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -68px; -} - -.tsd-kind-function.tsd-has-type-parameter > .tsd-kind-icon:before { - background-position: -136px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -153px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class - > .tsd-kind-icon:before { - background-position: -51px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum - > .tsd-kind-icon:before { - background-position: -170px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-interface - > .tsd-kind-icon:before { - background-position: -204px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -85px; -} - -.tsd-kind-method.tsd-has-type-parameter > .tsd-kind-icon:before { - background-position: -136px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -153px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class - > .tsd-kind-icon:before { - background-position: -51px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum - > .tsd-kind-icon:before { - background-position: -170px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-interface - > .tsd-kind-icon:before { - background-position: -204px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -85px; -} - -.tsd-kind-constructor > .tsd-kind-icon:before { - background-position: -136px -102px; -} -.tsd-kind-constructor.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -102px; -} -.tsd-kind-constructor.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -102px; -} - -.tsd-kind-constructor-signature > .tsd-kind-icon:before { - background-position: -136px -102px; -} -.tsd-kind-constructor-signature.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -102px; -} -.tsd-kind-constructor-signature.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-interface - > .tsd-kind-icon:before { - background-position: -204px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -102px; -} - -.tsd-kind-index-signature > .tsd-kind-icon:before { - background-position: -136px -119px; -} -.tsd-kind-index-signature.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -119px; -} -.tsd-kind-index-signature.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -119px; -} - -.tsd-kind-event > .tsd-kind-icon:before { - background-position: -136px -136px; -} -.tsd-kind-event.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -136px; -} -.tsd-kind-event.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -136px; -} -.tsd-kind-event.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -136px; -} -.tsd-kind-event.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { - background-position: -68px -136px; -} -.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { - background-position: -85px -136px; -} -.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -136px; -} -.tsd-kind-event.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -136px; -} -.tsd-kind-event.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -136px; -} -.tsd-kind-event.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { - background-position: -187px -136px; -} -.tsd-kind-event.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -136px; -} -.tsd-kind-event.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -136px; -} -.tsd-kind-event.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -136px; -} - -.tsd-is-static > .tsd-kind-icon:before { - background-position: -136px -153px; -} -.tsd-is-static.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -153px; -} -.tsd-is-static.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -153px; -} -.tsd-is-static.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -153px; -} -.tsd-is-static.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { - background-position: -68px -153px; -} -.tsd-is-static.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { - background-position: -85px -153px; -} -.tsd-is-static.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -153px; -} -.tsd-is-static.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -153px; -} -.tsd-is-static.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -153px; -} -.tsd-is-static.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { - background-position: -187px -153px; -} -.tsd-is-static.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -153px; -} -.tsd-is-static.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -153px; -} -.tsd-is-static.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -153px; -} - -.tsd-is-static.tsd-kind-function > .tsd-kind-icon:before { - background-position: -136px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-interface - > .tsd-kind-icon:before { - background-position: -204px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -170px; -} - -.tsd-is-static.tsd-kind-method > .tsd-kind-icon:before { - background-position: -136px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-interface - > .tsd-kind-icon:before { - background-position: -204px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -170px; -} - -.tsd-is-static.tsd-kind-call-signature > .tsd-kind-icon:before { - background-position: -136px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -153px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class - > .tsd-kind-icon:before { - background-position: -51px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum - > .tsd-kind-icon:before { - background-position: -170px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-interface - > .tsd-kind-icon:before { - background-position: -204px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -170px; -} - -.tsd-is-static.tsd-kind-event > .tsd-kind-icon:before { - background-position: -136px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-interface - > .tsd-kind-icon:before { - background-position: -204px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -187px; -} diff --git a/frontend/docs/assets/icons.png b/frontend/docs/assets/icons.png deleted file mode 100644 index 3836d5fe46e48bbe186116855aae879c23935327..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9615 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 NZmR35l_G4O{0}dcmS_L~ diff --git a/frontend/docs/assets/icons@2x.png b/frontend/docs/assets/icons@2x.png deleted file mode 100644 index 5a209e2f6d7f915cc9cb6fe7a4264c8be4db87b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28144 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 Ce=Object.create;var J=Object.defineProperty;var Pe=Object.getOwnPropertyDescriptor;var Oe=Object.getOwnPropertyNames;var Re=Object.getPrototypeOf,_e=Object.prototype.hasOwnProperty;var Me=t=>J(t,"__esModule",{value:!0});var Fe=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var De=(t,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Oe(e))!_e.call(t,i)&&(r||i!=="default")&&J(t,i,{get:()=>e[i],enumerable:!(n=Pe(e,i))||n.enumerable});return t},Ae=(t,e)=>De(Me(J(t!=null?Ce(Re(t)):{},"default",!e&&t&&t.__esModule?{get:()=>t.default,enumerable:!0}:{value:t,enumerable:!0})),t);var de=Fe((ce,he)=>{(function(){var t=function(e){var r=new t.Builder;return r.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),r.searchPipeline.add(t.stemmer),e.call(r,r),r.build()};t.version="2.3.9";t.utils={},t.utils.warn=function(e){return function(r){e.console&&console.warn&&console.warn(r)}}(this),t.utils.asString=function(e){return e==null?"":e.toString()},t.utils.clone=function(e){if(e==null)return e;for(var r=Object.create(null),n=Object.keys(e),i=0;i0){var h=t.utils.clone(r)||{};h.position=[a,l],h.index=s.length,s.push(new t.Token(n.slice(a,o),h))}a=o+1}}return s},t.tokenizer.separator=/[\s\-]+/;t.Pipeline=function(){this._stack=[]},t.Pipeline.registeredFunctions=Object.create(null),t.Pipeline.registerFunction=function(e,r){r in this.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+r),e.label=r,t.Pipeline.registeredFunctions[e.label]=e},t.Pipeline.warnIfFunctionNotRegistered=function(e){var r=e.label&&e.label in this.registeredFunctions;r||t.utils.warn(`Function is not registered with pipeline. This may cause problems when serialising the index. -`,e)},t.Pipeline.load=function(e){var r=new t.Pipeline;return e.forEach(function(n){var i=t.Pipeline.registeredFunctions[n];if(i)r.add(i);else throw new Error("Cannot load unregistered function: "+n)}),r},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(r){t.Pipeline.warnIfFunctionNotRegistered(r),this._stack.push(r)},this)},t.Pipeline.prototype.after=function(e,r){t.Pipeline.warnIfFunctionNotRegistered(r);var n=this._stack.indexOf(e);if(n==-1)throw new Error("Cannot find existingFn");n=n+1,this._stack.splice(n,0,r)},t.Pipeline.prototype.before=function(e,r){t.Pipeline.warnIfFunctionNotRegistered(r);var n=this._stack.indexOf(e);if(n==-1)throw new Error("Cannot find existingFn");this._stack.splice(n,0,r)},t.Pipeline.prototype.remove=function(e){var r=this._stack.indexOf(e);r!=-1&&this._stack.splice(r,1)},t.Pipeline.prototype.run=function(e){for(var r=this._stack.length,n=0;n1&&(oe&&(n=s),o!=e);)i=n-r,s=r+Math.floor(i/2),o=this.elements[s*2];if(o==e||o>e)return s*2;if(ou?h+=2:a==u&&(r+=n[l+1]*i[h+1],l+=2,h+=2);return r},t.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},t.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),r=1,n=0;r0){var o=s.str.charAt(0),a;o in s.node.edges?a=s.node.edges[o]:(a=new t.TokenSet,s.node.edges[o]=a),s.str.length==1&&(a.final=!0),i.push({node:a,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(s.editsRemaining!=0){if("*"in s.node.edges)var u=s.node.edges["*"];else{var u=new t.TokenSet;s.node.edges["*"]=u}if(s.str.length==0&&(u.final=!0),i.push({node:u,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&i.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),s.str.length==1&&(s.node.final=!0),s.str.length>=1){if("*"in s.node.edges)var l=s.node.edges["*"];else{var l=new t.TokenSet;s.node.edges["*"]=l}s.str.length==1&&(l.final=!0),i.push({node:l,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var h=s.str.charAt(0),p=s.str.charAt(1),v;p in s.node.edges?v=s.node.edges[p]:(v=new t.TokenSet,s.node.edges[p]=v),s.str.length==1&&(v.final=!0),i.push({node:v,editsRemaining:s.editsRemaining-1,str:h+s.str.slice(2)})}}}return n},t.TokenSet.fromString=function(e){for(var r=new t.TokenSet,n=r,i=0,s=e.length;i=e;r--){var n=this.uncheckedNodes[r],i=n.child.toString();i in this.minimizedNodes?n.parent.edges[n.char]=this.minimizedNodes[i]:(n.child._str=i,this.minimizedNodes[i]=n.child),this.uncheckedNodes.pop()}};t.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},t.Index.prototype.search=function(e){return this.query(function(r){var n=new t.QueryParser(e,r);n.parse()})},t.Index.prototype.query=function(e){for(var r=new t.Query(this.fields),n=Object.create(null),i=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),u=0;u1?this._b=1:this._b=e},t.Builder.prototype.k1=function(e){this._k1=e},t.Builder.prototype.add=function(e,r){var n=e[this._ref],i=Object.keys(this._fields);this._documents[n]=r||{},this.documentCount+=1;for(var s=0;s=this.length)return t.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},t.QueryLexer.prototype.width=function(){return this.pos-this.start},t.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},t.QueryLexer.prototype.backup=function(){this.pos-=1},t.QueryLexer.prototype.acceptDigitRun=function(){var e,r;do e=this.next(),r=e.charCodeAt(0);while(r>47&&r<58);e!=t.QueryLexer.EOS&&this.backup()},t.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(t.QueryLexer.TERM)),e.ignore(),e.more())return t.QueryLexer.lexText},t.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.EDIT_DISTANCE),t.QueryLexer.lexText},t.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.BOOST),t.QueryLexer.lexText},t.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(t.QueryLexer.TERM)},t.QueryLexer.termSeparator=t.tokenizer.separator,t.QueryLexer.lexText=function(e){for(;;){var r=e.next();if(r==t.QueryLexer.EOS)return t.QueryLexer.lexEOS;if(r.charCodeAt(0)==92){e.escapeCharacter();continue}if(r==":")return t.QueryLexer.lexField;if(r=="~")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexEditDistance;if(r=="^")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexBoost;if(r=="+"&&e.width()===1||r=="-"&&e.width()===1)return e.emit(t.QueryLexer.PRESENCE),t.QueryLexer.lexText;if(r.match(t.QueryLexer.termSeparator))return t.QueryLexer.lexTerm}},t.QueryParser=function(e,r){this.lexer=new t.QueryLexer(e),this.query=r,this.currentClause={},this.lexemeIdx=0},t.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=t.QueryParser.parseClause;e;)e=e(this);return this.query},t.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},t.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},t.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},t.QueryParser.parseClause=function(e){var r=e.peekLexeme();if(r!=null)switch(r.type){case t.QueryLexer.PRESENCE:return t.QueryParser.parsePresence;case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var n="expected either a field or a term, found "+r.type;throw r.str.length>=1&&(n+=" with value '"+r.str+"'"),new t.QueryParseError(n,r.start,r.end)}},t.QueryParser.parsePresence=function(e){var r=e.consumeLexeme();if(r!=null){switch(r.str){case"-":e.currentClause.presence=t.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=t.Query.presence.REQUIRED;break;default:var n="unrecognised presence operator'"+r.str+"'";throw new t.QueryParseError(n,r.start,r.end)}var i=e.peekLexeme();if(i==null){var n="expecting term or field, found nothing";throw new t.QueryParseError(n,r.start,r.end)}switch(i.type){case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var n="expecting term or field, found '"+i.type+"'";throw new t.QueryParseError(n,i.start,i.end)}}},t.QueryParser.parseField=function(e){var r=e.consumeLexeme();if(r!=null){if(e.query.allFields.indexOf(r.str)==-1){var n=e.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),i="unrecognised field '"+r.str+"', possible fields: "+n;throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.fields=[r.str];var s=e.peekLexeme();if(s==null){var i="expecting term, found nothing";throw new t.QueryParseError(i,r.start,r.end)}switch(s.type){case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var i="expecting term, found '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseTerm=function(e){var r=e.consumeLexeme();if(r!=null){e.currentClause.term=r.str.toLowerCase(),r.str.indexOf("*")!=-1&&(e.currentClause.usePipeline=!1);var n=e.peekLexeme();if(n==null){e.nextClause();return}switch(n.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+n.type+"'";throw new t.QueryParseError(i,n.start,n.end)}}},t.QueryParser.parseEditDistance=function(e){var r=e.consumeLexeme();if(r!=null){var n=parseInt(r.str,10);if(isNaN(n)){var i="edit distance must be numeric";throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.editDistance=n;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseBoost=function(e){var r=e.consumeLexeme();if(r!=null){var n=parseInt(r.str,10);if(isNaN(n)){var i="boost must be numeric";throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.boost=n;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},function(e,r){typeof define=="function"&&define.amd?define(r):typeof ce=="object"?he.exports=r():e.lunr=r()}(this,function(){return t})})()});var le=[];function N(t,e){le.push({selector:e,constructor:t})}var X=class{constructor(){this.createComponents(document.body)}createComponents(e){le.forEach(r=>{e.querySelectorAll(r.selector).forEach(n=>{n.dataset.hasInstance||(new r.constructor({el:n}),n.dataset.hasInstance=String(!0))})})}};var Q=class{constructor(e){this.el=e.el}};var Z=class{constructor(){this.listeners={}}addEventListener(e,r){e in this.listeners||(this.listeners[e]=[]),this.listeners[e].push(r)}removeEventListener(e,r){if(!(e in this.listeners))return;let n=this.listeners[e];for(let i=0,s=n.length;i{let r=Date.now();return(...n)=>{r+e-Date.now()<0&&(t(...n),r=Date.now())}};var ee=class extends Z{constructor(){super();this.scrollTop=0;this.lastY=0;this.width=0;this.height=0;this.showToolbar=!0;this.toolbar=document.querySelector(".tsd-page-toolbar"),this.secondaryNav=document.querySelector(".tsd-navigation.secondary"),window.addEventListener("scroll",K(()=>this.onScroll(),10)),window.addEventListener("resize",K(()=>this.onResize(),10)),this.onResize(),this.onScroll()}triggerResize(){let e=new CustomEvent("resize",{detail:{width:this.width,height:this.height}});this.dispatchEvent(e)}onResize(){this.width=window.innerWidth||0,this.height=window.innerHeight||0;let e=new CustomEvent("resize",{detail:{width:this.width,height:this.height}});this.dispatchEvent(e)}onScroll(){this.scrollTop=window.scrollY||0;let e=new CustomEvent("scroll",{detail:{scrollTop:this.scrollTop}});this.dispatchEvent(e),this.hideShowToolbar()}hideShowToolbar(){var r;let e=this.showToolbar;this.showToolbar=this.lastY>=this.scrollTop||this.scrollTop<=0,e!==this.showToolbar&&(this.toolbar.classList.toggle("tsd-page-toolbar--hide"),(r=this.secondaryNav)==null||r.classList.toggle("tsd-navigation--toolbar-hide")),this.lastY=this.scrollTop}},I=ee;I.instance=new ee;var te=class extends Q{constructor(e){super(e);this.anchors=[];this.index=-1;I.instance.addEventListener("resize",()=>this.onResize()),I.instance.addEventListener("scroll",r=>this.onScroll(r)),this.createAnchors()}createAnchors(){let e=window.location.href;e.indexOf("#")!=-1&&(e=e.substr(0,e.indexOf("#"))),this.el.querySelectorAll("a").forEach(r=>{let n=r.href;if(n.indexOf("#")==-1||n.substr(0,e.length)!=e)return;let i=n.substr(n.indexOf("#")+1),s=document.querySelector("a.tsd-anchor[name="+i+"]"),o=r.parentNode;!s||!o||this.anchors.push({link:o,anchor:s,position:0})}),this.onResize()}onResize(){let e;for(let n=0,i=this.anchors.length;nn.position-i.position);let r=new CustomEvent("scroll",{detail:{scrollTop:I.instance.scrollTop}});this.onScroll(r)}onScroll(e){let r=e.detail.scrollTop+5,n=this.anchors,i=n.length-1,s=this.index;for(;s>-1&&n[s].position>r;)s-=1;for(;s-1&&this.anchors[this.index].link.classList.remove("focus"),this.index=s,this.index>-1&&this.anchors[this.index].link.classList.add("focus"))}};var ue=(t,e=100)=>{let r;return(...n)=>{clearTimeout(r),r=setTimeout(()=>t(n),e)}};var me=Ae(de());function ve(){let t=document.getElementById("tsd-search");if(!t)return;let e=document.getElementById("search-script");t.classList.add("loading"),e&&(e.addEventListener("error",()=>{t.classList.remove("loading"),t.classList.add("failure")}),e.addEventListener("load",()=>{t.classList.remove("loading"),t.classList.add("ready")}),window.searchData&&t.classList.remove("loading"));let r=document.querySelector("#tsd-search input"),n=document.querySelector("#tsd-search .results");if(!r||!n)throw new Error("The input field or the result list wrapper was not found");let i=!1;n.addEventListener("mousedown",()=>i=!0),n.addEventListener("mouseup",()=>{i=!1,t.classList.remove("has-focus")}),r.addEventListener("focus",()=>t.classList.add("has-focus")),r.addEventListener("blur",()=>{i||(i=!1,t.classList.remove("has-focus"))});let s={base:t.dataset.base+"/"};Ve(t,n,r,s)}function Ve(t,e,r,n){r.addEventListener("input",ue(()=>{ze(t,e,r,n)},200));let i=!1;r.addEventListener("keydown",s=>{i=!0,s.key=="Enter"?Ne(e,r):s.key=="Escape"?r.blur():s.key=="ArrowUp"?fe(e,-1):s.key==="ArrowDown"?fe(e,1):i=!1}),r.addEventListener("keypress",s=>{i&&s.preventDefault()}),document.body.addEventListener("keydown",s=>{s.altKey||s.ctrlKey||s.metaKey||!r.matches(":focus")&&s.key==="/"&&(r.focus(),s.preventDefault())})}function He(t,e){t.index||window.searchData&&(e.classList.remove("loading"),e.classList.add("ready"),t.data=window.searchData,t.index=me.Index.load(window.searchData.index))}function ze(t,e,r,n){if(He(n,t),!n.index||!n.data)return;e.textContent="";let i=r.value.trim(),s=n.index.search(`*${i}*`);for(let o=0,a=Math.min(10,s.length);o${pe(u.parent,i)}.${l}`);let h=document.createElement("li");h.classList.value=u.classes;let p=document.createElement("a");p.href=n.base+u.url,p.classList.add("tsd-kind-icon"),p.innerHTML=l,h.append(p),e.appendChild(h)}}function fe(t,e){let r=t.querySelector(".current");if(!r)r=t.querySelector(e==1?"li:first-child":"li:last-child"),r&&r.classList.add("current");else{let n=r;if(e===1)do n=n.nextElementSibling;while(n instanceof HTMLElement&&n.offsetParent==null);else do n=n.previousElementSibling;while(n instanceof HTMLElement&&n.offsetParent==null);n&&(r.classList.remove("current"),n.classList.add("current"))}}function Ne(t,e){let r=t.querySelector(".current");if(r||(r=t.querySelector("li:first-child")),r){let n=r.querySelector("a");n&&(window.location.href=n.href),e.blur()}}function pe(t,e){if(e==="")return t;let r=t.toLocaleLowerCase(),n=e.toLocaleLowerCase(),i=[],s=0,o=r.indexOf(n);for(;o!=-1;)i.push(re(t.substring(s,o)),`${re(t.substring(o,o+n.length))}`),s=o+n.length,o=r.indexOf(n,s);return i.push(re(t.substring(s))),i.join("")}var je={"&":"&","<":"<",">":">","'":"'",'"':"""};function re(t){return t.replace(/[&<>"'"]/g,e=>je[e])}var ge=class{constructor(e,r){this.signature=e,this.description=r}addClass(e){return this.signature.classList.add(e),this.description.classList.add(e),this}removeClass(e){return this.signature.classList.remove(e),this.description.classList.remove(e),this}},ne=class extends Q{constructor(e){super(e);this.groups=[];this.index=-1;this.createGroups(),this.container&&(this.el.classList.add("active"),Array.from(this.el.children).forEach(r=>{r.addEventListener("touchstart",n=>this.onClick(n)),r.addEventListener("click",n=>this.onClick(n))}),this.container.classList.add("active"),this.setIndex(0))}setIndex(e){if(e<0&&(e=0),e>this.groups.length-1&&(e=this.groups.length-1),this.index==e)return;let r=this.groups[e];if(this.index>-1){let n=this.groups[this.index];n.removeClass("current").addClass("fade-out"),r.addClass("current"),r.addClass("fade-in"),I.instance.triggerResize(),setTimeout(()=>{n.removeClass("fade-out"),r.removeClass("fade-in")},300)}else r.addClass("current"),I.instance.triggerResize();this.index=e}createGroups(){let e=this.el.children;if(e.length<2)return;this.container=this.el.nextElementSibling;let r=this.container.children;this.groups=[];for(let n=0;n{r.signature===e.currentTarget&&this.setIndex(n)})}};var C="mousedown",xe="mousemove",_="mouseup",G={x:0,y:0},ye=!1,ie=!1,Be=!1,A=!1,Le=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);document.documentElement.classList.add(Le?"is-mobile":"not-mobile");Le&&"ontouchstart"in document.documentElement&&(Be=!0,C="touchstart",xe="touchmove",_="touchend");document.addEventListener(C,t=>{ie=!0,A=!1;let e=C=="touchstart"?t.targetTouches[0]:t;G.y=e.pageY||0,G.x=e.pageX||0});document.addEventListener(xe,t=>{if(!!ie&&!A){let e=C=="touchstart"?t.targetTouches[0]:t,r=G.x-(e.pageX||0),n=G.y-(e.pageY||0);A=Math.sqrt(r*r+n*n)>10}});document.addEventListener(_,()=>{ie=!1});document.addEventListener("click",t=>{ye&&(t.preventDefault(),t.stopImmediatePropagation(),ye=!1)});var se=class extends Q{constructor(e){super(e);this.className=this.el.dataset.toggle||"",this.el.addEventListener(_,r=>this.onPointerUp(r)),this.el.addEventListener("click",r=>r.preventDefault()),document.addEventListener(C,r=>this.onDocumentPointerDown(r)),document.addEventListener(_,r=>this.onDocumentPointerUp(r))}setActive(e){if(this.active==e)return;this.active=e,document.documentElement.classList.toggle("has-"+this.className,e),this.el.classList.toggle("active",e);let r=(this.active?"to-has-":"from-has-")+this.className;document.documentElement.classList.add(r),setTimeout(()=>document.documentElement.classList.remove(r),500)}onPointerUp(e){A||(this.setActive(!0),e.preventDefault())}onDocumentPointerDown(e){if(this.active){if(e.target.closest(".col-menu, .tsd-filter-group"))return;this.setActive(!1)}}onDocumentPointerUp(e){if(!A&&this.active&&e.target.closest(".col-menu")){let r=e.target.closest("a");if(r){let n=window.location.href;n.indexOf("#")!=-1&&(n=n.substr(0,n.indexOf("#"))),r.href.substr(0,n.length)==n&&setTimeout(()=>this.setActive(!1),250)}}}};var ae=class{constructor(e,r){this.key=e,this.value=r,this.defaultValue=r,this.initialize(),window.localStorage[this.key]&&this.setValue(this.fromLocalStorage(window.localStorage[this.key]))}initialize(){}setValue(e){if(this.value==e)return;let r=this.value;this.value=e,window.localStorage[this.key]=this.toLocalStorage(e),this.handleValueChange(r,e)}},oe=class extends ae{initialize(){let e=document.querySelector("#tsd-filter-"+this.key);!e||(this.checkbox=e,this.checkbox.addEventListener("change",()=>{this.setValue(this.checkbox.checked)}))}handleValueChange(e,r){!this.checkbox||(this.checkbox.checked=this.value,document.documentElement.classList.toggle("toggle-"+this.key,this.value!=this.defaultValue))}fromLocalStorage(e){return e=="true"}toLocalStorage(e){return e?"true":"false"}},Ee=class extends ae{initialize(){document.documentElement.classList.add("toggle-"+this.key+this.value);let e=document.querySelector("#tsd-filter-"+this.key);if(!e)return;this.select=e;let r=()=>{this.select.classList.add("active")},n=()=>{this.select.classList.remove("active")};this.select.addEventListener(C,r),this.select.addEventListener("mouseover",r),this.select.addEventListener("mouseleave",n),this.select.querySelectorAll("li").forEach(i=>{i.addEventListener(_,s=>{e.classList.remove("active"),this.setValue(s.target.dataset.value||"")})}),document.addEventListener(C,i=>{this.select.contains(i.target)||this.select.classList.remove("active")})}handleValueChange(e,r){this.select.querySelectorAll("li.selected").forEach(s=>{s.classList.remove("selected")});let n=this.select.querySelector('li[data-value="'+r+'"]'),i=this.select.querySelector(".tsd-select-label");n&&i&&(n.classList.add("selected"),i.textContent=n.textContent),document.documentElement.classList.remove("toggle-"+e),document.documentElement.classList.add("toggle-"+r)}fromLocalStorage(e){return e}toLocalStorage(e){return e}},Y=class extends Q{constructor(e){super(e);this.optionVisibility=new Ee("visibility","private"),this.optionInherited=new oe("inherited",!0),this.optionExternals=new oe("externals",!0)}static isSupported(){try{return typeof window.localStorage!="undefined"}catch{return!1}}};function we(t){let e=localStorage.getItem("tsd-theme")||"os";t.value=e,be(e),t.addEventListener("change",()=>{localStorage.setItem("tsd-theme",t.value),be(t.value)})}function be(t){switch(t){case"os":document.body.classList.remove("light","dark");break;case"light":document.body.classList.remove("dark"),document.body.classList.add("light");break;case"dark":document.body.classList.remove("light"),document.body.classList.add("dark");break}}ve();N(te,".menu-highlight");N(ne,".tsd-signatures");N(se,"a[data-toggle]");Y.isSupported()?N(Y,"#tsd-filter"):document.documentElement.classList.add("no-filter");var Te=document.getElementById("theme");Te&&we(Te);var qe=new X;Object.defineProperty(window,"app",{value:qe});})(); -/*! - * lunr.Builder - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.Index - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.Pipeline - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.Set - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.TokenSet - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.Vector - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.stemmer - * Copyright (C) 2020 Oliver Nightingale - * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt - */ -/*! - * lunr.stopWordFilter - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.tokenizer - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.trimmer - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.utils - * Copyright (C) 2020 Oliver Nightingale - */ -/** - * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 - * Copyright (C) 2020 Oliver Nightingale - * @license MIT - */ diff --git a/frontend/docs/assets/search.js b/frontend/docs/assets/search.js deleted file mode 100644 index 3c0bcbf41..000000000 --- a/frontend/docs/assets/search.js +++ /dev/null @@ -1 +0,0 @@ -window.searchData = JSON.parse("{\"kinds\":{\"2\":\"Module\",\"4\":\"Namespace\",\"8\":\"Enumeration\",\"16\":\"Enumeration member\",\"64\":\"Function\",\"256\":\"Interface\",\"1024\":\"Property\",\"2048\":\"Method\"},\"rows\":[{\"id\":0,\"kind\":2,\"name\":\"App\",\"url\":\"modules/App.html\",\"classes\":\"tsd-kind-module\"},{\"id\":1,\"kind\":64,\"name\":\"default\",\"url\":\"modules/App.html#default\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"App\"},{\"id\":2,\"kind\":2,\"name\":\"Router\",\"url\":\"modules/Router.html\",\"classes\":\"tsd-kind-module\"},{\"id\":3,\"kind\":64,\"name\":\"default\",\"url\":\"modules/Router.html#default\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"Router\"},{\"id\":4,\"kind\":2,\"name\":\"components\",\"url\":\"modules/components.html\",\"classes\":\"tsd-kind-module\"},{\"id\":5,\"kind\":2,\"name\":\"contexts\",\"url\":\"modules/contexts.html\",\"classes\":\"tsd-kind-module\"},{\"id\":6,\"kind\":2,\"name\":\"data\",\"url\":\"modules/data.html\",\"classes\":\"tsd-kind-module\"},{\"id\":7,\"kind\":2,\"name\":\"utils\",\"url\":\"modules/utils.html\",\"classes\":\"tsd-kind-module\"},{\"id\":8,\"kind\":2,\"name\":\"views\",\"url\":\"modules/views.html\",\"classes\":\"tsd-kind-module\"},{\"id\":9,\"kind\":64,\"name\":\"AdminRoute\",\"url\":\"modules/components.html#AdminRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":10,\"kind\":64,\"name\":\"Footer\",\"url\":\"modules/components.html#Footer\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":11,\"kind\":4,\"name\":\"LoginComponents\",\"url\":\"modules/components.LoginComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":12,\"kind\":64,\"name\":\"WelcomeText\",\"url\":\"modules/components.LoginComponents.html#WelcomeText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":13,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/components.LoginComponents.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":14,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/components.LoginComponents.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":15,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/components.LoginComponents.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.LoginComponents\"},{\"id\":16,\"kind\":64,\"name\":\"NavBar\",\"url\":\"modules/components.html#NavBar\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":17,\"kind\":64,\"name\":\"OSOCLetters\",\"url\":\"modules/components.html#OSOCLetters\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":18,\"kind\":64,\"name\":\"PrivateRoute\",\"url\":\"modules/components.html#PrivateRoute\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":19,\"kind\":4,\"name\":\"RegisterComponents\",\"url\":\"modules/components.RegisterComponents.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"components\"},{\"id\":20,\"kind\":64,\"name\":\"Email\",\"url\":\"modules/components.RegisterComponents.html#Email\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":21,\"kind\":64,\"name\":\"Name\",\"url\":\"modules/components.RegisterComponents.html#Name\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":22,\"kind\":64,\"name\":\"Password\",\"url\":\"modules/components.RegisterComponents.html#Password\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":23,\"kind\":64,\"name\":\"ConfirmPassword\",\"url\":\"modules/components.RegisterComponents.html#ConfirmPassword\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":24,\"kind\":64,\"name\":\"SocialButtons\",\"url\":\"modules/components.RegisterComponents.html#SocialButtons\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":25,\"kind\":64,\"name\":\"InfoText\",\"url\":\"modules/components.RegisterComponents.html#InfoText\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":26,\"kind\":64,\"name\":\"BadInviteLink\",\"url\":\"modules/components.RegisterComponents.html#BadInviteLink\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"components.RegisterComponents\"},{\"id\":27,\"kind\":256,\"name\":\"AuthContextState\",\"url\":\"interfaces/contexts.AuthContextState.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-module\",\"parent\":\"contexts\"},{\"id\":28,\"kind\":1024,\"name\":\"isLoggedIn\",\"url\":\"interfaces/contexts.AuthContextState.html#isLoggedIn\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":29,\"kind\":2048,\"name\":\"setIsLoggedIn\",\"url\":\"interfaces/contexts.AuthContextState.html#setIsLoggedIn\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":30,\"kind\":1024,\"name\":\"role\",\"url\":\"interfaces/contexts.AuthContextState.html#role\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":31,\"kind\":2048,\"name\":\"setRole\",\"url\":\"interfaces/contexts.AuthContextState.html#setRole\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":32,\"kind\":1024,\"name\":\"token\",\"url\":\"interfaces/contexts.AuthContextState.html#token\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":33,\"kind\":2048,\"name\":\"setToken\",\"url\":\"interfaces/contexts.AuthContextState.html#setToken\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":34,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/contexts.AuthContextState.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":35,\"kind\":2048,\"name\":\"setEditions\",\"url\":\"interfaces/contexts.AuthContextState.html#setEditions\",\"classes\":\"tsd-kind-method tsd-parent-kind-interface\",\"parent\":\"contexts.AuthContextState\"},{\"id\":36,\"kind\":64,\"name\":\"AuthProvider\",\"url\":\"modules/contexts.html#AuthProvider\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"contexts\"},{\"id\":37,\"kind\":4,\"name\":\"Enums\",\"url\":\"modules/data.Enums.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"data\"},{\"id\":38,\"kind\":8,\"name\":\"StorageKey\",\"url\":\"enums/data.Enums.StorageKey.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"data.Enums\"},{\"id\":39,\"kind\":16,\"name\":\"BEARER_TOKEN\",\"url\":\"enums/data.Enums.StorageKey.html#BEARER_TOKEN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.StorageKey\"},{\"id\":40,\"kind\":8,\"name\":\"Role\",\"url\":\"enums/data.Enums.Role.html\",\"classes\":\"tsd-kind-enum tsd-parent-kind-namespace\",\"parent\":\"data.Enums\"},{\"id\":41,\"kind\":16,\"name\":\"ADMIN\",\"url\":\"enums/data.Enums.Role.html#ADMIN\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.Role\"},{\"id\":42,\"kind\":16,\"name\":\"COACH\",\"url\":\"enums/data.Enums.Role.html#COACH\",\"classes\":\"tsd-kind-enum-member tsd-parent-kind-enum\",\"parent\":\"data.Enums.Role\"},{\"id\":43,\"kind\":4,\"name\":\"Interfaces\",\"url\":\"modules/data.Interfaces.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"data\"},{\"id\":44,\"kind\":256,\"name\":\"User\",\"url\":\"interfaces/data.Interfaces.User.html\",\"classes\":\"tsd-kind-interface tsd-parent-kind-namespace\",\"parent\":\"data.Interfaces\"},{\"id\":45,\"kind\":1024,\"name\":\"userId\",\"url\":\"interfaces/data.Interfaces.User.html#userId\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":46,\"kind\":1024,\"name\":\"name\",\"url\":\"interfaces/data.Interfaces.User.html#name\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":47,\"kind\":1024,\"name\":\"admin\",\"url\":\"interfaces/data.Interfaces.User.html#admin\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":48,\"kind\":1024,\"name\":\"editions\",\"url\":\"interfaces/data.Interfaces.User.html#editions\",\"classes\":\"tsd-kind-property tsd-parent-kind-interface\",\"parent\":\"data.Interfaces.User\"},{\"id\":49,\"kind\":4,\"name\":\"Api\",\"url\":\"modules/utils.Api.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"utils\"},{\"id\":50,\"kind\":64,\"name\":\"validateRegistrationUrl\",\"url\":\"modules/utils.Api.html#validateRegistrationUrl\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api\"},{\"id\":51,\"kind\":64,\"name\":\"setBearerToken\",\"url\":\"modules/utils.Api.html#setBearerToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.Api\"},{\"id\":52,\"kind\":4,\"name\":\"LocalStorage\",\"url\":\"modules/utils.LocalStorage.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"utils\"},{\"id\":53,\"kind\":64,\"name\":\"getToken\",\"url\":\"modules/utils.LocalStorage.html#getToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.LocalStorage\"},{\"id\":54,\"kind\":64,\"name\":\"setToken\",\"url\":\"modules/utils.LocalStorage.html#setToken\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"utils.LocalStorage\"},{\"id\":55,\"kind\":4,\"name\":\"Errors\",\"url\":\"modules/views.Errors.html\",\"classes\":\"tsd-kind-namespace tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":56,\"kind\":64,\"name\":\"ForbiddenPage\",\"url\":\"modules/views.Errors.html#ForbiddenPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"views.Errors\"},{\"id\":57,\"kind\":64,\"name\":\"NotFoundPage\",\"url\":\"modules/views.Errors.html#NotFoundPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-namespace\",\"parent\":\"views.Errors\"},{\"id\":58,\"kind\":64,\"name\":\"LoginPage\",\"url\":\"modules/views.html#LoginPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":59,\"kind\":64,\"name\":\"PendingPage\",\"url\":\"modules/views.html#PendingPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":60,\"kind\":64,\"name\":\"ProjectsPage\",\"url\":\"modules/views.html#ProjectsPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":61,\"kind\":64,\"name\":\"RegisterPage\",\"url\":\"modules/views.html#RegisterPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":62,\"kind\":64,\"name\":\"StudentsPage\",\"url\":\"modules/views.html#StudentsPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":63,\"kind\":64,\"name\":\"UsersPage\",\"url\":\"modules/views.html#UsersPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"},{\"id\":64,\"kind\":64,\"name\":\"VerifyingTokenPage\",\"url\":\"modules/views.html#VerifyingTokenPage\",\"classes\":\"tsd-kind-function tsd-parent-kind-module\",\"parent\":\"views\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"parent\"],\"fieldVectors\":[[\"name/0\",[0,32.734]],[\"parent/0\",[]],[\"name/1\",[1,32.734]],[\"parent/1\",[0,3.119]],[\"name/2\",[2,32.734]],[\"parent/2\",[]],[\"name/3\",[1,32.734]],[\"parent/3\",[2,3.119]],[\"name/4\",[3,20.496]],[\"parent/4\",[]],[\"name/5\",[4,29.369]],[\"parent/5\",[]],[\"name/6\",[5,29.369]],[\"parent/6\",[]],[\"name/7\",[6,29.369]],[\"parent/7\",[]],[\"name/8\",[7,19.384]],[\"parent/8\",[]],[\"name/9\",[8,37.842]],[\"parent/9\",[3,1.953]],[\"name/10\",[9,37.842]],[\"parent/10\",[3,1.953]],[\"name/11\",[10,37.842]],[\"parent/11\",[3,1.953]],[\"name/12\",[11,37.842]],[\"parent/12\",[12,2.559]],[\"name/13\",[13,32.734]],[\"parent/13\",[12,2.559]],[\"name/14\",[14,32.734]],[\"parent/14\",[12,2.559]],[\"name/15\",[15,32.734]],[\"parent/15\",[12,2.559]],[\"name/16\",[16,37.842]],[\"parent/16\",[3,1.953]],[\"name/17\",[17,37.842]],[\"parent/17\",[3,1.953]],[\"name/18\",[18,37.842]],[\"parent/18\",[3,1.953]],[\"name/19\",[19,37.842]],[\"parent/19\",[3,1.953]],[\"name/20\",[15,32.734]],[\"parent/20\",[20,2.072]],[\"name/21\",[21,32.734]],[\"parent/21\",[20,2.072]],[\"name/22\",[14,32.734]],[\"parent/22\",[20,2.072]],[\"name/23\",[22,37.842]],[\"parent/23\",[20,2.072]],[\"name/24\",[13,32.734]],[\"parent/24\",[20,2.072]],[\"name/25\",[23,37.842]],[\"parent/25\",[20,2.072]],[\"name/26\",[24,37.842]],[\"parent/26\",[20,2.072]],[\"name/27\",[25,37.842]],[\"parent/27\",[4,2.799]],[\"name/28\",[26,37.842]],[\"parent/28\",[27,1.953]],[\"name/29\",[28,37.842]],[\"parent/29\",[27,1.953]],[\"name/30\",[29,32.734]],[\"parent/30\",[27,1.953]],[\"name/31\",[30,37.842]],[\"parent/31\",[27,1.953]],[\"name/32\",[31,37.842]],[\"parent/32\",[27,1.953]],[\"name/33\",[32,32.734]],[\"parent/33\",[27,1.953]],[\"name/34\",[33,32.734]],[\"parent/34\",[27,1.953]],[\"name/35\",[34,37.842]],[\"parent/35\",[27,1.953]],[\"name/36\",[35,37.842]],[\"parent/36\",[4,2.799]],[\"name/37\",[36,37.842]],[\"parent/37\",[5,2.799]],[\"name/38\",[37,37.842]],[\"parent/38\",[38,3.119]],[\"name/39\",[39,37.842]],[\"parent/39\",[40,3.606]],[\"name/40\",[29,32.734]],[\"parent/40\",[38,3.119]],[\"name/41\",[41,32.734]],[\"parent/41\",[42,3.119]],[\"name/42\",[43,37.842]],[\"parent/42\",[42,3.119]],[\"name/43\",[44,37.842]],[\"parent/43\",[5,2.799]],[\"name/44\",[45,37.842]],[\"parent/44\",[46,3.606]],[\"name/45\",[47,37.842]],[\"parent/45\",[48,2.559]],[\"name/46\",[21,32.734]],[\"parent/46\",[48,2.559]],[\"name/47\",[41,32.734]],[\"parent/47\",[48,2.559]],[\"name/48\",[33,32.734]],[\"parent/48\",[48,2.559]],[\"name/49\",[49,37.842]],[\"parent/49\",[6,2.799]],[\"name/50\",[50,37.842]],[\"parent/50\",[51,3.119]],[\"name/51\",[52,37.842]],[\"parent/51\",[51,3.119]],[\"name/52\",[53,37.842]],[\"parent/52\",[6,2.799]],[\"name/53\",[54,37.842]],[\"parent/53\",[55,3.119]],[\"name/54\",[32,32.734]],[\"parent/54\",[55,3.119]],[\"name/55\",[56,37.842]],[\"parent/55\",[7,1.847]],[\"name/56\",[57,37.842]],[\"parent/56\",[58,3.119]],[\"name/57\",[59,37.842]],[\"parent/57\",[58,3.119]],[\"name/58\",[60,37.842]],[\"parent/58\",[7,1.847]],[\"name/59\",[61,37.842]],[\"parent/59\",[7,1.847]],[\"name/60\",[62,37.842]],[\"parent/60\",[7,1.847]],[\"name/61\",[63,37.842]],[\"parent/61\",[7,1.847]],[\"name/62\",[64,37.842]],[\"parent/62\",[7,1.847]],[\"name/63\",[65,37.842]],[\"parent/63\",[7,1.847]],[\"name/64\",[66,37.842]],[\"parent/64\",[7,1.847]]],\"invertedIndex\":[[\"admin\",{\"_index\":41,\"name\":{\"41\":{},\"47\":{}},\"parent\":{}}],[\"adminroute\",{\"_index\":8,\"name\":{\"9\":{}},\"parent\":{}}],[\"api\",{\"_index\":49,\"name\":{\"49\":{}},\"parent\":{}}],[\"app\",{\"_index\":0,\"name\":{\"0\":{}},\"parent\":{\"1\":{}}}],[\"authcontextstate\",{\"_index\":25,\"name\":{\"27\":{}},\"parent\":{}}],[\"authprovider\",{\"_index\":35,\"name\":{\"36\":{}},\"parent\":{}}],[\"badinvitelink\",{\"_index\":24,\"name\":{\"26\":{}},\"parent\":{}}],[\"bearer_token\",{\"_index\":39,\"name\":{\"39\":{}},\"parent\":{}}],[\"coach\",{\"_index\":43,\"name\":{\"42\":{}},\"parent\":{}}],[\"components\",{\"_index\":3,\"name\":{\"4\":{}},\"parent\":{\"9\":{},\"10\":{},\"11\":{},\"16\":{},\"17\":{},\"18\":{},\"19\":{}}}],[\"components.logincomponents\",{\"_index\":12,\"name\":{},\"parent\":{\"12\":{},\"13\":{},\"14\":{},\"15\":{}}}],[\"components.registercomponents\",{\"_index\":20,\"name\":{},\"parent\":{\"20\":{},\"21\":{},\"22\":{},\"23\":{},\"24\":{},\"25\":{},\"26\":{}}}],[\"confirmpassword\",{\"_index\":22,\"name\":{\"23\":{}},\"parent\":{}}],[\"contexts\",{\"_index\":4,\"name\":{\"5\":{}},\"parent\":{\"27\":{},\"36\":{}}}],[\"contexts.authcontextstate\",{\"_index\":27,\"name\":{},\"parent\":{\"28\":{},\"29\":{},\"30\":{},\"31\":{},\"32\":{},\"33\":{},\"34\":{},\"35\":{}}}],[\"data\",{\"_index\":5,\"name\":{\"6\":{}},\"parent\":{\"37\":{},\"43\":{}}}],[\"data.enums\",{\"_index\":38,\"name\":{},\"parent\":{\"38\":{},\"40\":{}}}],[\"data.enums.role\",{\"_index\":42,\"name\":{},\"parent\":{\"41\":{},\"42\":{}}}],[\"data.enums.storagekey\",{\"_index\":40,\"name\":{},\"parent\":{\"39\":{}}}],[\"data.interfaces\",{\"_index\":46,\"name\":{},\"parent\":{\"44\":{}}}],[\"data.interfaces.user\",{\"_index\":48,\"name\":{},\"parent\":{\"45\":{},\"46\":{},\"47\":{},\"48\":{}}}],[\"default\",{\"_index\":1,\"name\":{\"1\":{},\"3\":{}},\"parent\":{}}],[\"editions\",{\"_index\":33,\"name\":{\"34\":{},\"48\":{}},\"parent\":{}}],[\"email\",{\"_index\":15,\"name\":{\"15\":{},\"20\":{}},\"parent\":{}}],[\"enums\",{\"_index\":36,\"name\":{\"37\":{}},\"parent\":{}}],[\"errors\",{\"_index\":56,\"name\":{\"55\":{}},\"parent\":{}}],[\"footer\",{\"_index\":9,\"name\":{\"10\":{}},\"parent\":{}}],[\"forbiddenpage\",{\"_index\":57,\"name\":{\"56\":{}},\"parent\":{}}],[\"gettoken\",{\"_index\":54,\"name\":{\"53\":{}},\"parent\":{}}],[\"infotext\",{\"_index\":23,\"name\":{\"25\":{}},\"parent\":{}}],[\"interfaces\",{\"_index\":44,\"name\":{\"43\":{}},\"parent\":{}}],[\"isloggedin\",{\"_index\":26,\"name\":{\"28\":{}},\"parent\":{}}],[\"localstorage\",{\"_index\":53,\"name\":{\"52\":{}},\"parent\":{}}],[\"logincomponents\",{\"_index\":10,\"name\":{\"11\":{}},\"parent\":{}}],[\"loginpage\",{\"_index\":60,\"name\":{\"58\":{}},\"parent\":{}}],[\"name\",{\"_index\":21,\"name\":{\"21\":{},\"46\":{}},\"parent\":{}}],[\"navbar\",{\"_index\":16,\"name\":{\"16\":{}},\"parent\":{}}],[\"notfoundpage\",{\"_index\":59,\"name\":{\"57\":{}},\"parent\":{}}],[\"osocletters\",{\"_index\":17,\"name\":{\"17\":{}},\"parent\":{}}],[\"password\",{\"_index\":14,\"name\":{\"14\":{},\"22\":{}},\"parent\":{}}],[\"pendingpage\",{\"_index\":61,\"name\":{\"59\":{}},\"parent\":{}}],[\"privateroute\",{\"_index\":18,\"name\":{\"18\":{}},\"parent\":{}}],[\"projectspage\",{\"_index\":62,\"name\":{\"60\":{}},\"parent\":{}}],[\"registercomponents\",{\"_index\":19,\"name\":{\"19\":{}},\"parent\":{}}],[\"registerpage\",{\"_index\":63,\"name\":{\"61\":{}},\"parent\":{}}],[\"role\",{\"_index\":29,\"name\":{\"30\":{},\"40\":{}},\"parent\":{}}],[\"router\",{\"_index\":2,\"name\":{\"2\":{}},\"parent\":{\"3\":{}}}],[\"setbearertoken\",{\"_index\":52,\"name\":{\"51\":{}},\"parent\":{}}],[\"seteditions\",{\"_index\":34,\"name\":{\"35\":{}},\"parent\":{}}],[\"setisloggedin\",{\"_index\":28,\"name\":{\"29\":{}},\"parent\":{}}],[\"setrole\",{\"_index\":30,\"name\":{\"31\":{}},\"parent\":{}}],[\"settoken\",{\"_index\":32,\"name\":{\"33\":{},\"54\":{}},\"parent\":{}}],[\"socialbuttons\",{\"_index\":13,\"name\":{\"13\":{},\"24\":{}},\"parent\":{}}],[\"storagekey\",{\"_index\":37,\"name\":{\"38\":{}},\"parent\":{}}],[\"studentspage\",{\"_index\":64,\"name\":{\"62\":{}},\"parent\":{}}],[\"token\",{\"_index\":31,\"name\":{\"32\":{}},\"parent\":{}}],[\"user\",{\"_index\":45,\"name\":{\"44\":{}},\"parent\":{}}],[\"userid\",{\"_index\":47,\"name\":{\"45\":{}},\"parent\":{}}],[\"userspage\",{\"_index\":65,\"name\":{\"63\":{}},\"parent\":{}}],[\"utils\",{\"_index\":6,\"name\":{\"7\":{}},\"parent\":{\"49\":{},\"52\":{}}}],[\"utils.api\",{\"_index\":51,\"name\":{},\"parent\":{\"50\":{},\"51\":{}}}],[\"utils.localstorage\",{\"_index\":55,\"name\":{},\"parent\":{\"53\":{},\"54\":{}}}],[\"validateregistrationurl\",{\"_index\":50,\"name\":{\"50\":{}},\"parent\":{}}],[\"verifyingtokenpage\",{\"_index\":66,\"name\":{\"64\":{}},\"parent\":{}}],[\"views\",{\"_index\":7,\"name\":{\"8\":{}},\"parent\":{\"55\":{},\"58\":{},\"59\":{},\"60\":{},\"61\":{},\"62\":{},\"63\":{},\"64\":{}}}],[\"views.errors\",{\"_index\":58,\"name\":{},\"parent\":{\"56\":{},\"57\":{}}}],[\"welcometext\",{\"_index\":11,\"name\":{\"12\":{}},\"parent\":{}}]],\"pipeline\":[]}}"); \ No newline at end of file diff --git a/frontend/docs/assets/style.css b/frontend/docs/assets/style.css deleted file mode 100644 index a16ed029e..000000000 --- a/frontend/docs/assets/style.css +++ /dev/null @@ -1,1413 +0,0 @@ -@import url("./icons.css"); - -:root { - /* Light */ - --light-color-background: #fcfcfc; - --light-color-secondary-background: #fff; - --light-color-text: #222; - --light-color-text-aside: #707070; - --light-color-link: #4da6ff; - --light-color-menu-divider: #eee; - --light-color-menu-divider-focus: #000; - --light-color-menu-label: #707070; - --light-color-panel: var(--light-color-secondary-background); - --light-color-panel-divider: #eee; - --light-color-comment-tag: #707070; - --light-color-comment-tag-text: #fff; - --light-color-ts: #9600ff; - --light-color-ts-interface: #647f1b; - --light-color-ts-enum: #937210; - --light-color-ts-class: #0672de; - --light-color-ts-private: #707070; - --light-color-toolbar: #fff; - --light-color-toolbar-text: #333; - --light-icon-filter: invert(0); - --light-external-icon: url("data:image/svg+xml;utf8,"); - - /* Dark */ - --dark-color-background: #36393f; - --dark-color-secondary-background: #2f3136; - --dark-color-text: #ffffff; - --dark-color-text-aside: #e6e4e4; - --dark-color-link: #00aff4; - --dark-color-menu-divider: #eee; - --dark-color-menu-divider-focus: #000; - --dark-color-menu-label: #707070; - --dark-color-panel: var(--dark-color-secondary-background); - --dark-color-panel-divider: #818181; - --dark-color-comment-tag: #dcddde; - --dark-color-comment-tag-text: #2f3136; - --dark-color-ts: #c97dff; - --dark-color-ts-interface: #9cbe3c; - --dark-color-ts-enum: #d6ab29; - --dark-color-ts-class: #3695f3; - --dark-color-ts-private: #e2e2e2; - --dark-color-toolbar: #34373c; - --dark-color-toolbar-text: #ffffff; - --dark-icon-filter: invert(1); - --dark-external-icon: url("data:image/svg+xml;utf8,"); -} - -@media (prefers-color-scheme: light) { - :root { - --color-background: var(--light-color-background); - --color-secondary-background: var(--light-color-secondary-background); - --color-text: var(--light-color-text); - --color-text-aside: var(--light-color-text-aside); - --color-link: var(--light-color-link); - --color-menu-divider: var(--light-color-menu-divider); - --color-menu-divider-focus: var(--light-color-menu-divider-focus); - --color-menu-label: var(--light-color-menu-label); - --color-panel: var(--light-color-panel); - --color-panel-divider: var(--light-color-panel-divider); - --color-comment-tag: var(--light-color-comment-tag); - --color-comment-tag-text: var(--light-color-comment-tag-text); - --color-ts: var(--light-color-ts); - --color-ts-interface: var(--light-color-ts-interface); - --color-ts-enum: var(--light-color-ts-enum); - --color-ts-class: var(--light-color-ts-class); - --color-ts-private: var(--light-color-ts-private); - --color-toolbar: var(--light-color-toolbar); - --color-toolbar-text: var(--light-color-toolbar-text); - --icon-filter: var(--light-icon-filter); - --external-icon: var(--light-external-icon); - } -} - -@media (prefers-color-scheme: dark) { - :root { - --color-background: var(--dark-color-background); - --color-secondary-background: var(--dark-color-secondary-background); - --color-text: var(--dark-color-text); - --color-text-aside: var(--dark-color-text-aside); - --color-link: var(--dark-color-link); - --color-menu-divider: var(--dark-color-menu-divider); - --color-menu-divider-focus: var(--dark-color-menu-divider-focus); - --color-menu-label: var(--dark-color-menu-label); - --color-panel: var(--dark-color-panel); - --color-panel-divider: var(--dark-color-panel-divider); - --color-comment-tag: var(--dark-color-comment-tag); - --color-comment-tag-text: var(--dark-color-comment-tag-text); - --color-ts: var(--dark-color-ts); - --color-ts-interface: var(--dark-color-ts-interface); - --color-ts-enum: var(--dark-color-ts-enum); - --color-ts-class: var(--dark-color-ts-class); - --color-ts-private: var(--dark-color-ts-private); - --color-toolbar: var(--dark-color-toolbar); - --color-toolbar-text: var(--dark-color-toolbar-text); - --icon-filter: var(--dark-icon-filter); - --external-icon: var(--dark-external-icon); - } -} - -body { - margin: 0; -} - -body.light { - --color-background: var(--light-color-background); - --color-secondary-background: var(--light-color-secondary-background); - --color-text: var(--light-color-text); - --color-text-aside: var(--light-color-text-aside); - --color-link: var(--light-color-link); - --color-menu-divider: var(--light-color-menu-divider); - --color-menu-divider-focus: var(--light-color-menu-divider-focus); - --color-menu-label: var(--light-color-menu-label); - --color-panel: var(--light-color-panel); - --color-panel-divider: var(--light-color-panel-divider); - --color-comment-tag: var(--light-color-comment-tag); - --color-comment-tag-text: var(--light-color-comment-tag-text); - --color-ts: var(--light-color-ts); - --color-ts-interface: var(--light-color-ts-interface); - --color-ts-enum: var(--light-color-ts-enum); - --color-ts-class: var(--light-color-ts-class); - --color-ts-private: var(--light-color-ts-private); - --color-toolbar: var(--light-color-toolbar); - --color-toolbar-text: var(--light-color-toolbar-text); - --icon-filter: var(--light-icon-filter); - --external-icon: var(--light-external-icon); -} - -body.dark { - --color-background: var(--dark-color-background); - --color-secondary-background: var(--dark-color-secondary-background); - --color-text: var(--dark-color-text); - --color-text-aside: var(--dark-color-text-aside); - --color-link: var(--dark-color-link); - --color-menu-divider: var(--dark-color-menu-divider); - --color-menu-divider-focus: var(--dark-color-menu-divider-focus); - --color-menu-label: var(--dark-color-menu-label); - --color-panel: var(--dark-color-panel); - --color-panel-divider: var(--dark-color-panel-divider); - --color-comment-tag: var(--dark-color-comment-tag); - --color-comment-tag-text: var(--dark-color-comment-tag-text); - --color-ts: var(--dark-color-ts); - --color-ts-interface: var(--dark-color-ts-interface); - --color-ts-enum: var(--dark-color-ts-enum); - --color-ts-class: var(--dark-color-ts-class); - --color-ts-private: var(--dark-color-ts-private); - --color-toolbar: var(--dark-color-toolbar); - --color-toolbar-text: var(--dark-color-toolbar-text); - --icon-filter: var(--dark-icon-filter); - --external-icon: var(--dark-external-icon); -} - -h1, -h2, -h3, -h4, -h5, -h6 { - line-height: 1.2; -} - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -h2 { - font-size: 1.5em; - margin: 0.83em 0; -} - -h3 { - font-size: 1.17em; - margin: 1em 0; -} - -h4, -.tsd-index-panel h3 { - font-size: 1em; - margin: 1.33em 0; -} - -h5 { - font-size: 0.83em; - margin: 1.67em 0; -} - -h6 { - font-size: 0.67em; - margin: 2.33em 0; -} - -pre { - white-space: pre; - white-space: pre-wrap; - word-wrap: break-word; -} - -dl, -menu, -ol, -ul { - margin: 1em 0; -} - -dd { - margin: 0 0 0 40px; -} - -.container { - max-width: 1200px; - margin: 0 auto; - padding: 0 40px; -} -@media (max-width: 640px) { - .container { - padding: 0 20px; - } -} - -.container-main { - padding-bottom: 200px; -} - -.row { - display: flex; - position: relative; - margin: 0 -10px; -} -.row:after { - visibility: hidden; - display: block; - content: ""; - clear: both; - height: 0; -} - -.col-4, -.col-8 { - box-sizing: border-box; - float: left; - padding: 0 10px; -} - -.col-4 { - width: 33.3333333333%; -} -.col-8 { - width: 66.6666666667%; -} - -ul.tsd-descriptions > li > :first-child, -.tsd-panel > :first-child, -.col-8 > :first-child, -.col-4 > :first-child, -ul.tsd-descriptions > li > :first-child > :first-child, -.tsd-panel > :first-child > :first-child, -.col-8 > :first-child > :first-child, -.col-4 > :first-child > :first-child, -ul.tsd-descriptions > li > :first-child > :first-child > :first-child, -.tsd-panel > :first-child > :first-child > :first-child, -.col-8 > :first-child > :first-child > :first-child, -.col-4 > :first-child > :first-child > :first-child { - margin-top: 0; -} -ul.tsd-descriptions > li > :last-child, -.tsd-panel > :last-child, -.col-8 > :last-child, -.col-4 > :last-child, -ul.tsd-descriptions > li > :last-child > :last-child, -.tsd-panel > :last-child > :last-child, -.col-8 > :last-child > :last-child, -.col-4 > :last-child > :last-child, -ul.tsd-descriptions > li > :last-child > :last-child > :last-child, -.tsd-panel > :last-child > :last-child > :last-child, -.col-8 > :last-child > :last-child > :last-child, -.col-4 > :last-child > :last-child > :last-child { - margin-bottom: 0; -} - -@keyframes fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } -} -@keyframes fade-out { - from { - opacity: 1; - visibility: visible; - } - to { - opacity: 0; - } -} -@keyframes fade-in-delayed { - 0% { - opacity: 0; - } - 33% { - opacity: 0; - } - 100% { - opacity: 1; - } -} -@keyframes fade-out-delayed { - 0% { - opacity: 1; - visibility: visible; - } - 66% { - opacity: 0; - } - 100% { - opacity: 0; - } -} -@keyframes shift-to-left { - from { - transform: translate(0, 0); - } - to { - transform: translate(-25%, 0); - } -} -@keyframes unshift-to-left { - from { - transform: translate(-25%, 0); - } - to { - transform: translate(0, 0); - } -} -@keyframes pop-in-from-right { - from { - transform: translate(100%, 0); - } - to { - transform: translate(0, 0); - } -} -@keyframes pop-out-to-right { - from { - transform: translate(0, 0); - visibility: visible; - } - to { - transform: translate(100%, 0); - } -} -body { - background: var(--color-background); - font-family: "Segoe UI", sans-serif; - font-size: 16px; - color: var(--color-text); -} - -a { - color: var(--color-link); - text-decoration: none; -} -a:hover { - text-decoration: underline; -} -a.external[target="_blank"] { - background-image: var(--external-icon); - background-position: top 3px right; - background-repeat: no-repeat; - padding-right: 13px; -} - -code, -pre { - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; - padding: 0.2em; - margin: 0; - font-size: 14px; -} - -pre { - padding: 10px; -} -pre code { - padding: 0; - font-size: 100%; -} - -blockquote { - margin: 1em 0; - padding-left: 1em; - border-left: 4px solid gray; -} - -.tsd-typography { - line-height: 1.333em; -} -.tsd-typography ul { - list-style: square; - padding: 0 0 0 20px; - margin: 0; -} -.tsd-typography h4, -.tsd-typography .tsd-index-panel h3, -.tsd-index-panel .tsd-typography h3, -.tsd-typography h5, -.tsd-typography h6 { - font-size: 1em; - margin: 0; -} -.tsd-typography h5, -.tsd-typography h6 { - font-weight: normal; -} -.tsd-typography p, -.tsd-typography ul, -.tsd-typography ol { - margin: 1em 0; -} - -@media (min-width: 901px) and (max-width: 1024px) { - html .col-content { - width: 72%; - } - html .col-menu { - width: 28%; - } - html .tsd-navigation { - padding-left: 10px; - } -} -@media (max-width: 900px) { - html .col-content { - float: none; - width: 100%; - } - html .col-menu { - position: fixed !important; - overflow: auto; - -webkit-overflow-scrolling: touch; - z-index: 1024; - top: 0 !important; - bottom: 0 !important; - left: auto !important; - right: 0 !important; - width: 100%; - padding: 20px 20px 0 0; - max-width: 450px; - visibility: hidden; - background-color: var(--color-panel); - transform: translate(100%, 0); - } - html .col-menu > *:last-child { - padding-bottom: 20px; - } - html .overlay { - content: ""; - display: block; - position: fixed; - z-index: 1023; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.75); - visibility: hidden; - } - - .to-has-menu .overlay { - animation: fade-in 0.4s; - } - - .to-has-menu :is(header, footer, .col-content) { - animation: shift-to-left 0.4s; - } - - .to-has-menu .col-menu { - animation: pop-in-from-right 0.4s; - } - - .from-has-menu .overlay { - animation: fade-out 0.4s; - } - - .from-has-menu :is(header, footer, .col-content) { - animation: unshift-to-left 0.4s; - } - - .from-has-menu .col-menu { - animation: pop-out-to-right 0.4s; - } - - .has-menu body { - overflow: hidden; - } - .has-menu .overlay { - visibility: visible; - } - .has-menu :is(header, footer, .col-content) { - transform: translate(-25%, 0); - } - .has-menu .col-menu { - visibility: visible; - transform: translate(0, 0); - display: grid; - grid-template-rows: auto 1fr; - max-height: 100vh; - } - .has-menu .tsd-navigation { - max-height: 100%; - } -} - -.tsd-page-title { - padding: 70px 0 20px 0; - margin: 0 0 40px 0; - background: var(--color-panel); - box-shadow: 0 0 5px rgba(0, 0, 0, 0.35); -} -.tsd-page-title h1 { - margin: 0; -} - -.tsd-breadcrumb { - margin: 0; - padding: 0; - color: var(--color-text-aside); -} -.tsd-breadcrumb a { - color: var(--color-text-aside); - text-decoration: none; -} -.tsd-breadcrumb a:hover { - text-decoration: underline; -} -.tsd-breadcrumb li { - display: inline; -} -.tsd-breadcrumb li:after { - content: " / "; -} - -dl.tsd-comment-tags { - overflow: hidden; -} -dl.tsd-comment-tags dt { - float: left; - padding: 1px 5px; - margin: 0 10px 0 0; - border-radius: 4px; - border: 1px solid var(--color-comment-tag); - color: var(--color-comment-tag); - font-size: 0.8em; - font-weight: normal; -} -dl.tsd-comment-tags dd { - margin: 0 0 10px 0; -} -dl.tsd-comment-tags dd:before, -dl.tsd-comment-tags dd:after { - display: table; - content: " "; -} -dl.tsd-comment-tags dd pre, -dl.tsd-comment-tags dd:after { - clear: both; -} -dl.tsd-comment-tags p { - margin: 0; -} - -.tsd-panel.tsd-comment .lead { - font-size: 1.1em; - line-height: 1.333em; - margin-bottom: 2em; -} -.tsd-panel.tsd-comment .lead:last-child { - margin-bottom: 0; -} - -.toggle-protected .tsd-is-private { - display: none; -} - -.toggle-public .tsd-is-private, -.toggle-public .tsd-is-protected, -.toggle-public .tsd-is-private-protected { - display: none; -} - -.toggle-inherited .tsd-is-inherited { - display: none; -} - -.toggle-externals .tsd-is-external { - display: none; -} - -#tsd-filter { - position: relative; - display: inline-block; - height: 40px; - vertical-align: bottom; -} -.no-filter #tsd-filter { - display: none; -} -#tsd-filter .tsd-filter-group { - display: inline-block; - height: 40px; - vertical-align: bottom; - white-space: nowrap; -} -#tsd-filter input { - display: none; -} -@media (max-width: 900px) { - #tsd-filter .tsd-filter-group { - display: block; - position: absolute; - top: 40px; - right: 20px; - height: auto; - background-color: var(--color-panel); - visibility: hidden; - transform: translate(50%, 0); - box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); - } - .has-options #tsd-filter .tsd-filter-group { - visibility: visible; - } - .to-has-options #tsd-filter .tsd-filter-group { - animation: fade-in 0.2s; - } - .from-has-options #tsd-filter .tsd-filter-group { - animation: fade-out 0.2s; - } - #tsd-filter label, - #tsd-filter .tsd-select { - display: block; - padding-right: 20px; - } -} - -footer { - border-top: 1px solid var(--color-panel-divider); - background-color: var(--color-panel); -} -footer:after { - content: ""; - display: table; -} -footer.with-border-bottom { - border-bottom: 1px solid var(--color-panel-divider); -} -footer .tsd-legend-group { - font-size: 0; -} -footer .tsd-legend { - display: inline-block; - width: 25%; - padding: 0; - font-size: 16px; - list-style: none; - line-height: 1.333em; - vertical-align: top; -} -@media (max-width: 900px) { - footer .tsd-legend { - width: 50%; - } -} - -.tsd-hierarchy { - list-style: square; - padding: 0 0 0 20px; - margin: 0; -} -.tsd-hierarchy .target { - font-weight: bold; -} - -.tsd-index-panel .tsd-index-content { - margin-bottom: -30px !important; -} -.tsd-index-panel .tsd-index-section { - margin-bottom: 30px !important; -} -.tsd-index-panel h3 { - margin: 0 -20px 10px -20px; - padding: 0 20px 10px 20px; - border-bottom: 1px solid var(--color-panel-divider); -} -.tsd-index-panel ul.tsd-index-list { - -webkit-column-count: 3; - -moz-column-count: 3; - -ms-column-count: 3; - -o-column-count: 3; - column-count: 3; - -webkit-column-gap: 20px; - -moz-column-gap: 20px; - -ms-column-gap: 20px; - -o-column-gap: 20px; - column-gap: 20px; - padding: 0; - list-style: none; - line-height: 1.333em; -} -@media (max-width: 900px) { - .tsd-index-panel ul.tsd-index-list { - -webkit-column-count: 1; - -moz-column-count: 1; - -ms-column-count: 1; - -o-column-count: 1; - column-count: 1; - } -} -@media (min-width: 901px) and (max-width: 1024px) { - .tsd-index-panel ul.tsd-index-list { - -webkit-column-count: 2; - -moz-column-count: 2; - -ms-column-count: 2; - -o-column-count: 2; - column-count: 2; - } -} -.tsd-index-panel ul.tsd-index-list li { - -webkit-page-break-inside: avoid; - -moz-page-break-inside: avoid; - -ms-page-break-inside: avoid; - -o-page-break-inside: avoid; - page-break-inside: avoid; -} -.tsd-index-panel a, -.tsd-index-panel .tsd-parent-kind-module a { - color: var(--color-ts); -} -.tsd-index-panel .tsd-parent-kind-interface a { - color: var(--color-ts-interface); -} -.tsd-index-panel .tsd-parent-kind-enum a { - color: var(--color-ts-enum); -} -.tsd-index-panel .tsd-parent-kind-class a { - color: var(--color-ts-class); -} -.tsd-index-panel .tsd-kind-module a { - color: var(--color-ts); -} -.tsd-index-panel .tsd-kind-interface a { - color: var(--color-ts-interface); -} -.tsd-index-panel .tsd-kind-enum a { - color: var(--color-ts-enum); -} -.tsd-index-panel .tsd-kind-class a { - color: var(--color-ts-class); -} -.tsd-index-panel .tsd-is-private a { - color: var(--color-ts-private); -} - -.tsd-flag { - display: inline-block; - padding: 1px 5px; - border-radius: 4px; - color: var(--color-comment-tag-text); - background-color: var(--color-comment-tag); - text-indent: 0; - font-size: 14px; - font-weight: normal; -} - -.tsd-anchor { - position: absolute; - top: -100px; -} - -.tsd-member { - position: relative; -} -.tsd-member .tsd-anchor + h3 { - margin-top: 0; - margin-bottom: 0; - border-bottom: none; -} -.tsd-member [data-tsd-kind] { - color: var(--color-ts); -} -.tsd-member [data-tsd-kind="Interface"] { - color: var(--color-ts-interface); -} -.tsd-member [data-tsd-kind="Enum"] { - color: var(--color-ts-enum); -} -.tsd-member [data-tsd-kind="Class"] { - color: var(--color-ts-class); -} -.tsd-member [data-tsd-kind="Private"] { - color: var(--color-ts-private); -} - -.tsd-navigation { - margin: 0 0 0 40px; -} -.tsd-navigation a { - display: block; - padding-top: 2px; - padding-bottom: 2px; - border-left: 2px solid transparent; - color: var(--color-text); - text-decoration: none; - transition: border-left-color 0.1s; -} -.tsd-navigation a:hover { - text-decoration: underline; -} -.tsd-navigation ul { - margin: 0; - padding: 0; - list-style: none; -} -.tsd-navigation li { - padding: 0; -} - -.tsd-navigation.primary { - padding-bottom: 40px; -} -.tsd-navigation.primary a { - display: block; - padding-top: 6px; - padding-bottom: 6px; -} -.tsd-navigation.primary ul li a { - padding-left: 5px; -} -.tsd-navigation.primary ul li li a { - padding-left: 25px; -} -.tsd-navigation.primary ul li li li a { - padding-left: 45px; -} -.tsd-navigation.primary ul li li li li a { - padding-left: 65px; -} -.tsd-navigation.primary ul li li li li li a { - padding-left: 85px; -} -.tsd-navigation.primary ul li li li li li li a { - padding-left: 105px; -} -.tsd-navigation.primary > ul { - border-bottom: 1px solid var(--color-panel-divider); -} -.tsd-navigation.primary li { - border-top: 1px solid var(--color-panel-divider); -} -.tsd-navigation.primary li.current > a { - font-weight: bold; -} -.tsd-navigation.primary li.label span { - display: block; - padding: 20px 0 6px 5px; - color: var(--color-menu-label); -} -.tsd-navigation.primary li.globals + li > span, -.tsd-navigation.primary li.globals + li > a { - padding-top: 20px; -} - -.tsd-navigation.secondary { - max-height: calc(100vh - 1rem - 40px); - overflow: auto; - position: sticky; - top: calc(0.5rem + 40px); - transition: 0.3s; -} -.tsd-navigation.secondary.tsd-navigation--toolbar-hide { - max-height: calc(100vh - 1rem); - top: 0.5rem; -} -.tsd-navigation.secondary ul { - transition: opacity 0.2s; -} -.tsd-navigation.secondary ul li a { - padding-left: 25px; -} -.tsd-navigation.secondary ul li li a { - padding-left: 45px; -} -.tsd-navigation.secondary ul li li li a { - padding-left: 65px; -} -.tsd-navigation.secondary ul li li li li a { - padding-left: 85px; -} -.tsd-navigation.secondary ul li li li li li a { - padding-left: 105px; -} -.tsd-navigation.secondary ul li li li li li li a { - padding-left: 125px; -} -.tsd-navigation.secondary ul.current a { - border-left-color: var(--color-panel-divider); -} -.tsd-navigation.secondary li.focus > a, -.tsd-navigation.secondary ul.current li.focus > a { - border-left-color: var(--color-menu-divider-focus); -} -.tsd-navigation.secondary li.current { - margin-top: 20px; - margin-bottom: 20px; - border-left-color: var(--color-panel-divider); -} -.tsd-navigation.secondary li.current > a { - font-weight: bold; -} - -@media (min-width: 901px) { - .menu-sticky-wrap { - position: static; - } -} - -.tsd-panel { - margin: 20px 0; - padding: 20px; - background-color: var(--color-panel); - box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); -} -.tsd-panel:empty { - display: none; -} -.tsd-panel > h1, -.tsd-panel > h2, -.tsd-panel > h3 { - margin: 1.5em -20px 10px -20px; - padding: 0 20px 10px 20px; - border-bottom: 1px solid var(--color-panel-divider); -} -.tsd-panel > h1.tsd-before-signature, -.tsd-panel > h2.tsd-before-signature, -.tsd-panel > h3.tsd-before-signature { - margin-bottom: 0; - border-bottom: 0; -} -.tsd-panel table { - display: block; - width: 100%; - overflow: auto; - margin-top: 10px; - word-break: normal; - word-break: keep-all; - border-collapse: collapse; -} -.tsd-panel table th { - font-weight: bold; -} -.tsd-panel table th, -.tsd-panel table td { - padding: 6px 13px; - border: 1px solid var(--color-panel-divider); -} -.tsd-panel table tr { - background: var(--color-background); -} -.tsd-panel table tr:nth-child(even) { - background: var(--color-secondary-background); -} - -.tsd-panel-group { - margin: 60px 0; -} -.tsd-panel-group > h1, -.tsd-panel-group > h2, -.tsd-panel-group > h3 { - padding-left: 20px; - padding-right: 20px; -} - -#tsd-search { - transition: background-color 0.2s; -} -#tsd-search .title { - position: relative; - z-index: 2; -} -#tsd-search .field { - position: absolute; - left: 0; - top: 0; - right: 40px; - height: 40px; -} -#tsd-search .field input { - box-sizing: border-box; - position: relative; - top: -50px; - z-index: 1; - width: 100%; - padding: 0 10px; - opacity: 0; - outline: 0; - border: 0; - background: transparent; - color: var(--color-text); -} -#tsd-search .field label { - position: absolute; - overflow: hidden; - right: -40px; -} -#tsd-search .field input, -#tsd-search .title { - transition: opacity 0.2s; -} -#tsd-search .results { - position: absolute; - visibility: hidden; - top: 40px; - width: 100%; - margin: 0; - padding: 0; - list-style: none; - box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); -} -#tsd-search .results li { - padding: 0 10px; - background-color: var(--color-background); -} -#tsd-search .results li:nth-child(even) { - background-color: var(--color-panel); -} -#tsd-search .results li.state { - display: none; -} -#tsd-search .results li.current, -#tsd-search .results li:hover { - background-color: var(--color-panel-divider); -} -#tsd-search .results a { - display: block; -} -#tsd-search .results a:before { - top: 10px; -} -#tsd-search .results span.parent { - color: var(--color-text-aside); - font-weight: normal; -} -#tsd-search.has-focus { - background-color: var(--color-panel-divider); -} -#tsd-search.has-focus .field input { - top: 0; - opacity: 1; -} -#tsd-search.has-focus .title { - z-index: 0; - opacity: 0; -} -#tsd-search.has-focus .results { - visibility: visible; -} -#tsd-search.loading .results li.state.loading { - display: block; -} -#tsd-search.failure .results li.state.failure { - display: block; -} - -.tsd-signature { - margin: 0 0 1em 0; - padding: 10px; - border: 1px solid var(--color-panel-divider); - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; - font-size: 14px; - overflow-x: auto; -} -.tsd-signature.tsd-kind-icon { - padding-left: 30px; -} -.tsd-signature.tsd-kind-icon:before { - top: 10px; - left: 10px; -} -.tsd-panel > .tsd-signature { - margin-left: -20px; - margin-right: -20px; - border-width: 1px 0; -} -.tsd-panel > .tsd-signature.tsd-kind-icon { - padding-left: 40px; -} -.tsd-panel > .tsd-signature.tsd-kind-icon:before { - left: 20px; -} - -.tsd-signature-symbol { - color: var(--color-text-aside); - font-weight: normal; -} - -.tsd-signature-type { - font-style: italic; - font-weight: normal; -} - -.tsd-signatures { - padding: 0; - margin: 0 0 1em 0; - border: 1px solid var(--color-panel-divider); -} -.tsd-signatures .tsd-signature { - margin: 0; - border-width: 1px 0 0 0; - transition: background-color 0.1s; -} -.tsd-signatures .tsd-signature:first-child { - border-top-width: 0; -} -.tsd-signatures .tsd-signature.current { - background-color: var(--color-panel-divider); -} -.tsd-signatures.active > .tsd-signature { - cursor: pointer; -} -.tsd-panel > .tsd-signatures { - margin-left: -20px; - margin-right: -20px; - border-width: 1px 0; -} -.tsd-panel > .tsd-signatures .tsd-signature.tsd-kind-icon { - padding-left: 40px; -} -.tsd-panel > .tsd-signatures .tsd-signature.tsd-kind-icon:before { - left: 20px; -} -.tsd-panel > a.anchor + .tsd-signatures { - border-top-width: 0; - margin-top: -20px; -} - -ul.tsd-descriptions { - position: relative; - overflow: hidden; - padding: 0; - list-style: none; -} -ul.tsd-descriptions.active > .tsd-description { - display: none; -} -ul.tsd-descriptions.active > .tsd-description.current { - display: block; -} -ul.tsd-descriptions.active > .tsd-description.fade-in { - animation: fade-in-delayed 0.3s; -} -ul.tsd-descriptions.active > .tsd-description.fade-out { - animation: fade-out-delayed 0.3s; - position: absolute; - display: block; - top: 0; - left: 0; - right: 0; - opacity: 0; - visibility: hidden; -} -ul.tsd-descriptions h4, -ul.tsd-descriptions .tsd-index-panel h3, -.tsd-index-panel ul.tsd-descriptions h3 { - font-size: 16px; - margin: 1em 0 0.5em 0; -} - -ul.tsd-parameters, -ul.tsd-type-parameters { - list-style: square; - margin: 0; - padding-left: 20px; -} -ul.tsd-parameters > li.tsd-parameter-signature, -ul.tsd-type-parameters > li.tsd-parameter-signature { - list-style: none; - margin-left: -20px; -} -ul.tsd-parameters h5, -ul.tsd-type-parameters h5 { - font-size: 16px; - margin: 1em 0 0.5em 0; -} -ul.tsd-parameters .tsd-comment, -ul.tsd-type-parameters .tsd-comment { - margin-top: -0.5em; -} - -.tsd-sources { - font-size: 14px; - color: var(--color-text-aside); - margin: 0 0 1em 0; -} -.tsd-sources a { - color: var(--color-text-aside); - text-decoration: underline; -} -.tsd-sources ul, -.tsd-sources p { - margin: 0 !important; -} -.tsd-sources ul { - list-style: none; - padding: 0; -} - -.tsd-page-toolbar { - position: fixed; - z-index: 1; - top: 0; - left: 0; - width: 100%; - height: 40px; - color: var(--color-toolbar-text); - background: var(--color-toolbar); - border-bottom: 1px solid var(--color-panel-divider); - transition: transform 0.3s linear; -} -.tsd-page-toolbar a { - color: var(--color-toolbar-text); - text-decoration: none; -} -.tsd-page-toolbar a.title { - font-weight: bold; -} -.tsd-page-toolbar a.title:hover { - text-decoration: underline; -} -.tsd-page-toolbar .table-wrap { - display: table; - width: 100%; - height: 40px; -} -.tsd-page-toolbar .table-cell { - display: table-cell; - position: relative; - white-space: nowrap; - line-height: 40px; -} -.tsd-page-toolbar .table-cell:first-child { - width: 100%; -} - -.tsd-page-toolbar--hide { - transform: translateY(-100%); -} - -.tsd-select .tsd-select-list li:before, -.tsd-select .tsd-select-label:before, -.tsd-widget:before { - content: ""; - display: inline-block; - width: 40px; - height: 40px; - margin: 0 -8px 0 0; - background-image: url(./widgets.png); - background-repeat: no-repeat; - text-indent: -1024px; - vertical-align: bottom; - filter: var(--icon-filter); -} -@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { - .tsd-select .tsd-select-list li:before, - .tsd-select .tsd-select-label:before, - .tsd-widget:before { - background-image: url(./widgets@2x.png); - background-size: 320px 40px; - } -} - -.tsd-widget { - display: inline-block; - overflow: hidden; - opacity: 0.8; - height: 40px; - transition: opacity 0.1s, background-color 0.2s; - vertical-align: bottom; - cursor: pointer; -} -.tsd-widget:hover { - opacity: 0.9; -} -.tsd-widget.active { - opacity: 1; - background-color: var(--color-panel-divider); -} -.tsd-widget.no-caption { - width: 40px; -} -.tsd-widget.no-caption:before { - margin: 0; -} -.tsd-widget.search:before { - background-position: 0 0; -} -.tsd-widget.menu:before { - background-position: -40px 0; -} -.tsd-widget.options:before { - background-position: -80px 0; -} -.tsd-widget.options, -.tsd-widget.menu { - display: none; -} -@media (max-width: 900px) { - .tsd-widget.options, - .tsd-widget.menu { - display: inline-block; - } -} -input[type="checkbox"] + .tsd-widget:before { - background-position: -120px 0; -} -input[type="checkbox"]:checked + .tsd-widget:before { - background-position: -160px 0; -} - -.tsd-select { - position: relative; - display: inline-block; - height: 40px; - transition: opacity 0.1s, background-color 0.2s; - vertical-align: bottom; - cursor: pointer; -} -.tsd-select .tsd-select-label { - opacity: 0.6; - transition: opacity 0.2s; -} -.tsd-select .tsd-select-label:before { - background-position: -240px 0; -} -.tsd-select.active .tsd-select-label { - opacity: 0.8; -} -.tsd-select.active .tsd-select-list { - visibility: visible; - opacity: 1; - transition-delay: 0s; -} -.tsd-select .tsd-select-list { - position: absolute; - visibility: hidden; - top: 40px; - left: 0; - margin: 0; - padding: 0; - opacity: 0; - list-style: none; - box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); - transition: visibility 0s 0.2s, opacity 0.2s; -} -.tsd-select .tsd-select-list li { - padding: 0 20px 0 0; - background-color: var(--color-background); -} -.tsd-select .tsd-select-list li:before { - background-position: 40px 0; -} -.tsd-select .tsd-select-list li:nth-child(even) { - background-color: var(--color-panel); -} -.tsd-select .tsd-select-list li:hover { - background-color: var(--color-panel-divider); -} -.tsd-select .tsd-select-list li.selected:before { - background-position: -200px 0; -} -@media (max-width: 900px) { - .tsd-select .tsd-select-list { - top: 0; - left: auto; - right: 100%; - margin-right: -5px; - } - .tsd-select .tsd-select-label:before { - background-position: -280px 0; - } -} - -img { - max-width: 100%; -} - -.tsd-anchor-icon { - margin-left: 10px; - vertical-align: middle; - color: var(--color-text); -} - -.tsd-anchor-icon svg { - width: 1em; - height: 1em; - visibility: hidden; -} - -.tsd-anchor-link:hover > .tsd-anchor-icon svg { - visibility: visible; -} diff --git a/frontend/docs/assets/widgets.png b/frontend/docs/assets/widgets.png deleted file mode 100644 index c7380532ac1b45400620011c37c4dcb7aec27a4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 480 zcmeAS@N?(olHy`uVBq!ia0y~yU~~YoH8@y+q^jrZML>b&o-U3d6^w6h1+IPUz|;DW zIZ;96kdsD>Qv^q=09&hp0GpEni<1IR%gvP3v%OR9*{MuRTKWHZyIbuBt)Ci`cU_&% z1T+i^Y)o{%281-<3TpPAUTzw5v;RY=>1rvxmPl96#kYc9hX!6V^nB|ad#(S+)}?8C zr_H+lT3B#So$T=?$(w3-{rbQ4R<@nsf$}$hwSO)A$8&`(j+wQf=Jwhb0`CvhR5DCf z^OgI)KQemrUFPH+UynC$Y~QHG%DbTVh-Skz{enNU)cV_hPu~{TD7TPZl>0&K>iuE| z7AYn$7)Jrb9GE&SfQW4q&G*@N|4cHI`VakFa5-C!ov&XD)J(qp$rJJ*9e z-sHv}#g*T7Cv048d1v~BEAzM5FztAse#q78WWC^BUCzQ U&wLp6h6BX&boFyt=akR{0G%$)mH+?% diff --git a/frontend/docs/assets/widgets@2x.png b/frontend/docs/assets/widgets@2x.png deleted file mode 100644 index 4bbbd57272f3b28f47527d4951ad10f950b8ad43..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 855 zcmeAS@N?(olHy`uVBq!ia0y~yU}^xe12~w0Jcmn z@(X6T|9^jgLcx21{)7exgY)a>N6m2F0<`Rqr;B4q1>>88jUdw-7W`c)zLE*mq8W2H z-<&Jl_Hco5BuC5n@AbF5GD82~-e8-v=#zCyUX0F-o}8pPfAv`!GN$ff+TL<~@kgt} z62eO?_|&+>xBmM$@p|z`tIKEdpPf8%qI>4r7@jn<=eta*{3~?g(zz{Ke9zc-G^gr? z-7foa?LcS!hmbwzru}ICvbWLlW8;+l-}!^=c32!^nV`+`C*;0-*Y%l94pC;Cb3GXz zzSf%a!{gVr{Y_lVuUj+a)*Ca+!-Hu%xmP&&X-2CuANY8^i{D7Kg6qzP zXz_ps9+lN8ESH{K4`yu&b~I>N9xGlE&;2u*b?+Go!AhN?m-bxlLvtC#MzDF2kFzfHJ1W7ybqdefSqVhbOykd*Yi%EDuhs z4wF{ft^bv2+DDnKb8gj1FuvcV`M}luS>lO<^)8x>y1#R;a=-ZKwWTQQb)ioBbi;zh zD!f5V)8581to1LL7c9!l^PSC$NBPYif!_vAZhmL4)v4U)4UsrLYiH_9rmQDd?)(e5 z^pcH>qvBg*i0dus2r*mp4;zKvu=P#s-ti;2obl`NjjwoYd>e(oo#j_uyRb<7Pv^If zzZ|mGHmV)8^tbO%^>eqMw(@7(&3g{jEp-Najo7V75xI_ZHK*FA`elF{r5}E*d7+j_R diff --git a/frontend/docs/enums/data.Enums.Role.html b/frontend/docs/enums/data.Enums.Role.html deleted file mode 100644 index 2c0e76c85..000000000 --- a/frontend/docs/enums/data.Enums.Role.html +++ /dev/null @@ -1,4 +0,0 @@ -Role | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu
                                        -

                                        Enum for the different levels of authority a user -can have

                                        -

                                        Index

                                        Enumeration members

                                        Enumeration members

                                        ADMIN = 0
                                        COACH = 1

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/enums/data.Enums.StorageKey.html b/frontend/docs/enums/data.Enums.StorageKey.html deleted file mode 100644 index a4449093e..000000000 --- a/frontend/docs/enums/data.Enums.StorageKey.html +++ /dev/null @@ -1,5 +0,0 @@ -StorageKey | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu
                                        -

                                        Enum for the keys in LocalStorage.

                                        -

                                        Index

                                        Enumeration members

                                        Enumeration members

                                        BEARER_TOKEN = "bearerToken"
                                        -

                                        Bearer token used to authorize the user's requests in the backend.

                                        -

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/index.html b/frontend/docs/index.html deleted file mode 100644 index 6ba31a5d3..000000000 --- a/frontend/docs/index.html +++ /dev/null @@ -1,131 +0,0 @@ -OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        OSOC 3 - Frontend Documentation

                                        - -

                                        Frontend

                                        -
                                        - - -

                                        Installing (and using) Yarn

                                        -
                                        -

                                        - What is my purpose?

                                        -

                                        - You install Yarn

                                        -
                                        npm install --global yarn
                                        -
                                        -

                                        :heavy_exclamation_mark: Do not use npm anymore! Yarn and npm shouldn't be used at the same -time.

                                        -
                                        # Installing new package
                                        yarn add <package_name>

                                        # Installing new package as a dev dependency
                                        yarn add --dev <package_name>

                                        # Installing all packages listed in package.json
                                        yarn install -
                                        - - -

                                        Setting up Prettier and ESLint

                                        -
                                        -

                                        This directory contains configuration files for Prettier and ESLint, and depending on your IDE -you may have to install or configure these in order for this to work.

                                        - - -

                                        Prettier

                                        -
                                        -

                                        Prettier is a code formatter that enforces your code to follow a specific style. Examples include -automatically adding semicolons (;) at the end of every line, converting single-quoted strings ('a') -to double-quoted strings ("a"), etc.

                                        - - -

                                        ESLint

                                        -
                                        -

                                        ESLint is, as the name suggests, a linter that reviews your code for bad practices and ugly -constructions.

                                        - - -

                                        JetBrains WebStorm

                                        -
                                        -

                                        When using WebStorm, Prettier and ESLint are supported by default. ESLint is enabled automatically -if a .eslintrc file is present, but you do have to enable Prettier in the settings.

                                        -
                                          -
                                        1. Make sure the packages were installed by running yarn install, as Prettier and ESLint are -among them.
                                        2. -
                                        3. In the search bar, type in "Prettier" (or navigate -to Languages & Frameworks > JavaScript > Prettier manually).
                                        4. -
                                        5. If the Prettier package-field is still empty, click the dropdown. WebStorm should automatically -list the Prettier from your local node-modules directory.
                                        6. -
                                        7. Select the On 'Reformat Code' action and On save checkboxes.
                                        8. -
                                        -

                                        Prettier WebStorm configuration

                                        - - -

                                        Visual Studio Code

                                        -
                                        -

                                        Visual Studio Code requires an extension for Prettier and ESLint to work, as they are not present in -the editor.

                                        -
                                          -
                                        1. Make sure the packages were installed by running yarn install, as Prettier and ESLint are -among them

                                          -
                                        2. -
                                        3. Install -the Prettier extension -.

                                          -
                                        4. -
                                        5. Install -the ESLint extension -.

                                          -
                                        6. -
                                        7. Select Prettier as the default formatter in the Editor: Default Formatter dropdown option.

                                          -

                                          VSCode: Default Formatter setting

                                          -
                                        8. -
                                        9. Enable the Editor: Format On Save option.

                                          -

                                          VSCode: Format On Save setting

                                          -
                                        10. -
                                        11. The path to the Prettier config file, and the module in node_modules should be detected ** -automatically**. In case it isn't (see Try it out!, you can always fill in the -fields in Prettier: Config Path and Prettier: Prettier Path.

                                          -
                                        12. -
                                        - - -

                                        Try it out!

                                        -
                                        -

                                        To test if your new settings work, you can try the following:

                                        -
                                          -
                                        1. Create a new TypeScript file, with any name (for example test.ts)

                                          -
                                        2. -
                                        3. In that file, add the following piece of code:

                                          -
                                          export const x = 5 // Don't add a semicolon here

                                          export function test() {
                                          // "variable" is never used, and never reassigned
                                          let variable = "something";
                                          } -
                                          -
                                        4. -
                                        5. Save the file by pressing ctrl + s

                                          -
                                        6. -
                                        7. Prettier: you should see a semicolon being added at the end of the line automatically

                                          -
                                        8. -
                                        9. ESLint: you should get a warning on variable telling you that it was never used, and also that -it should be marked as const because it's never reassigned.

                                          -
                                        10. -
                                        11. Don't forget to remove the test.ts file again :)

                                          -
                                        12. -
                                        - - -

                                        Available Scripts

                                        -
                                        -

                                        In the project directory, you can run:

                                        - - -

                                        yarn start

                                        -
                                        -

                                        Runs the app in the development mode.
                                        Open http://localhost:3000 to view it in the browser.

                                        -

                                        The page will reload if you make edits.
                                        You will also see any lint errors in the console.

                                        - - -

                                        yarn test

                                        -
                                        -

                                        Launches the test runner.

                                        - - -

                                        yarn build

                                        -
                                        -

                                        Builds the app for production to the build folder. It correctly bundles React in production mode -and optimizes the build for the best performance.

                                        -

                                        The build is minified and the filenames include the hashes.

                                        - - -

                                        yarn docs

                                        -
                                        -

                                        Auto-generates documentation for our code. The output can be found in the /docs folder.

                                        -

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/interfaces/contexts.AuthContextState.html b/frontend/docs/interfaces/contexts.AuthContextState.html deleted file mode 100644 index db218e95b..000000000 --- a/frontend/docs/interfaces/contexts.AuthContextState.html +++ /dev/null @@ -1,3 +0,0 @@ -AuthContextState | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu
                                        -

                                        Interface that holds the data stored in the AuthContext.

                                        -

                                        Hierarchy

                                        • AuthContextState

                                        Index

                                        Properties

                                        editions: number[]
                                        isLoggedIn: null | boolean
                                        role: null | Role
                                        token: null | string

                                        Methods

                                        • setEditions(value: number[]): void
                                        • setIsLoggedIn(value: null | boolean): void
                                        • setRole(value: null | Role): void
                                        • setToken(value: null | string): void

                                        Legend

                                        • Interface
                                        • Property
                                        • Method
                                        • Namespace
                                        • Function

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/interfaces/data.Interfaces.User.html b/frontend/docs/interfaces/data.Interfaces.User.html deleted file mode 100644 index 8671eeca9..000000000 --- a/frontend/docs/interfaces/data.Interfaces.User.html +++ /dev/null @@ -1,5 +0,0 @@ -User | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu
                                        -

                                        Data about a user using the application. -Contains a list of edition names so that we can quickly check if -they have access to a route or not.

                                        -

                                        Hierarchy

                                        • User

                                        Index

                                        Properties

                                        admin: boolean
                                        editions: string[]
                                        name: string
                                        userId: number

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface
                                        • Property

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/modules.html b/frontend/docs/modules.html deleted file mode 100644 index a3cb69eea..000000000 --- a/frontend/docs/modules.html +++ /dev/null @@ -1 +0,0 @@ -OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        OSOC 3 - Frontend Documentation

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/modules/App.html b/frontend/docs/modules/App.html deleted file mode 100644 index c535e7b86..000000000 --- a/frontend/docs/modules/App.html +++ /dev/null @@ -1,4 +0,0 @@ -App | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        Index

                                        Functions

                                        Functions

                                        • default(): Element

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/modules/Router.html b/frontend/docs/modules/Router.html deleted file mode 100644 index 1cf66e604..000000000 --- a/frontend/docs/modules/Router.html +++ /dev/null @@ -1,4 +0,0 @@ -Router | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        Index

                                        Functions

                                        Functions

                                        • default(): Element
                                        • -

                                          Router component to render different pages depending on the current url. Renders -the VerifyingTokenPage if the bearer token is still being validated.

                                          -

                                          Returns Element

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/modules/components.LoginComponents.html b/frontend/docs/modules/components.LoginComponents.html deleted file mode 100644 index b8b12739f..000000000 --- a/frontend/docs/modules/components.LoginComponents.html +++ /dev/null @@ -1,9 +0,0 @@ -LoginComponents | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        Index

                                        Functions

                                        • Email(__namedParameters: { email: string; setEmail: any }): Element
                                        • Password(__namedParameters: { password: string; callLogIn: any; setPassword: any }): Element
                                        • SocialButtons(): Element
                                        • WelcomeText(): Element

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/modules/components.RegisterComponents.html b/frontend/docs/modules/components.RegisterComponents.html deleted file mode 100644 index 3524a2733..000000000 --- a/frontend/docs/modules/components.RegisterComponents.html +++ /dev/null @@ -1,15 +0,0 @@ -RegisterComponents | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        Index

                                        Functions

                                        • BadInviteLink(): Element
                                        • ConfirmPassword(__namedParameters: { confirmPassword: string; callRegister: any; setConfirmPassword: any }): Element
                                        • Email(__namedParameters: { email: string; setEmail: any }): Element
                                        • InfoText(): Element
                                        • Name(__namedParameters: { name: string; setName: any }): Element
                                        • Password(__namedParameters: { password: string; setPassword: any }): Element
                                        • SocialButtons(): Element

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/modules/components.html b/frontend/docs/modules/components.html deleted file mode 100644 index 26769a478..000000000 --- a/frontend/docs/modules/components.html +++ /dev/null @@ -1,26 +0,0 @@ -components | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        Index

                                        Functions

                                        • AdminRoute(): Element
                                        • -

                                          React component for admin-only routes. -Redirects to the LoginPage (status 401) if not authenticated, -and to the ForbiddenPage (status 403) if not admin.

                                          -

                                          Example usage:

                                          -
                                          <Route path={"/path"} element={<AdminRoute />}>
                                          // These routes will only render if the user is an admin
                                          <Route path={"/"} />
                                          <Route path={"/child"} />
                                          </Route> -
                                          -

                                          Returns Element

                                        • Footer(): Element
                                        • -

                                          Footer placed at the bottom of the site, containing various links related -to the application or our code.

                                          -

                                          The footer is only displayed when signed in.

                                          -

                                          Returns Element

                                        • NavBar(): Element
                                        • -

                                          NavBar displayed at the top of the page. -Links are hidden if the user is not authorized to see them.

                                          -

                                          Returns Element

                                        • OSOCLetters(): Element
                                        • -

                                          Animated OSOC-letters, inspired by the ones found -on the OSOC website.

                                          -

                                          Note: This component is currently not in use because the positioning -of the letters causes issues. We have given priority to other parts of the application.

                                          -

                                          Returns Element

                                        • PrivateRoute(): Element
                                        • -

                                          React component that redirects to the LoginPage if not authenticated when -trying to visit a route.

                                          -

                                          Example usage:

                                          -
                                          <Route path={"/path"} element={<PrivateRoute />}>
                                          // These routes will only render if the user is authenticated
                                          <Route path={"/"} />
                                          <Route path={"/child"} />
                                          </Route> -
                                          -

                                          Returns Element

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/modules/contexts.html b/frontend/docs/modules/contexts.html deleted file mode 100644 index 312b17b06..000000000 --- a/frontend/docs/modules/contexts.html +++ /dev/null @@ -1,6 +0,0 @@ -contexts | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        Index

                                        Interfaces

                                        Functions

                                        Functions

                                        • AuthProvider(__namedParameters: { children: ReactNode }): Element
                                        • -

                                          Provider for auth that creates getters, setters, maintains state, and -provides default values.

                                          -

                                          This keeps the main App component code clean by handling this -boilerplate here instead.

                                          -

                                          Parameters

                                          • __namedParameters: { children: ReactNode }
                                            • children: ReactNode

                                          Returns Element

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/modules/data.Enums.html b/frontend/docs/modules/data.Enums.html deleted file mode 100644 index 4b652ff03..000000000 --- a/frontend/docs/modules/data.Enums.html +++ /dev/null @@ -1 +0,0 @@ -Enums | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/modules/data.Interfaces.html b/frontend/docs/modules/data.Interfaces.html deleted file mode 100644 index 573148cd1..000000000 --- a/frontend/docs/modules/data.Interfaces.html +++ /dev/null @@ -1 +0,0 @@ -Interfaces | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/modules/data.html b/frontend/docs/modules/data.html deleted file mode 100644 index bac91033f..000000000 --- a/frontend/docs/modules/data.html +++ /dev/null @@ -1 +0,0 @@ -data | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/modules/utils.Api.html b/frontend/docs/modules/utils.Api.html deleted file mode 100644 index 374ff7c4d..000000000 --- a/frontend/docs/modules/utils.Api.html +++ /dev/null @@ -1,7 +0,0 @@ -Api | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        Index

                                        Functions

                                        • setBearerToken(value: null | string): void
                                        • -

                                          Function to set the default bearer token in the request headers. -Passing null as the value will remove the header instead.

                                          -

                                          Parameters

                                          • value: null | string

                                          Returns void

                                        • validateRegistrationUrl(edition: string, uuid: string): Promise<boolean>
                                        • -

                                          Function to check if a registration url exists by sending a GET request, -if this returns a 200 then we know the url is valid.

                                          -

                                          Parameters

                                          • edition: string
                                          • uuid: string

                                          Returns Promise<boolean>

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/modules/utils.LocalStorage.html b/frontend/docs/modules/utils.LocalStorage.html deleted file mode 100644 index de2ce1baf..000000000 --- a/frontend/docs/modules/utils.LocalStorage.html +++ /dev/null @@ -1,6 +0,0 @@ -LocalStorage | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        Index

                                        Functions

                                        • getToken(): string | null
                                        • -

                                          Function to pull the user's token out of LocalStorage. -Returns null if there is no token in LocalStorage yet.

                                          -

                                          Returns string | null

                                        • setToken(value: null | string): void
                                        • -

                                          Function to set a new value for the bearer token in LocalStorage.

                                          -

                                          Parameters

                                          • value: null | string

                                          Returns void

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/modules/utils.html b/frontend/docs/modules/utils.html deleted file mode 100644 index 29a8d186b..000000000 --- a/frontend/docs/modules/utils.html +++ /dev/null @@ -1 +0,0 @@ -utils | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/modules/views.Errors.html b/frontend/docs/modules/views.Errors.html deleted file mode 100644 index bdf65408e..000000000 --- a/frontend/docs/modules/views.Errors.html +++ /dev/null @@ -1,7 +0,0 @@ -Errors | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        Index

                                        Functions

                                        • ForbiddenPage(): Element
                                        • -

                                          Page shown to users when they try to access a resource they aren't -authorized to. Examples include coaches performing admin actions, -or coaches going to urls for editions they aren't part of.

                                          -

                                          Returns Element

                                        • NotFoundPage(): Element

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file diff --git a/frontend/docs/modules/views.html b/frontend/docs/modules/views.html deleted file mode 100644 index 4d61bcd40..000000000 --- a/frontend/docs/modules/views.html +++ /dev/null @@ -1,12 +0,0 @@ -views | OSOC 3 - Frontend Documentation
                                        Options
                                        All
                                        • Public
                                        • Public/Protected
                                        • All
                                        Menu

                                        Index

                                        Functions

                                        • LoginPage(): Element
                                        • PendingPage(): Element
                                        • ProjectsPage(): Element
                                        • RegisterPage(): Element
                                        • StudentsPage(): Element
                                        • UsersPage(): Element
                                        • VerifyingTokenPage(): Element

                                        Legend

                                        • Namespace
                                        • Function
                                        • Interface

                                        Settings

                                        Theme

                                        \ No newline at end of file From 7b88ff7f6056a34b11ba94352cb613b6d7a65872 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Wed, 6 Apr 2022 11:16:26 +0200 Subject: [PATCH 242/536] add docs folder to gitignore --- frontend/.gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/.gitignore b/frontend/.gitignore index 4b293975d..6865e19be 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -28,4 +28,7 @@ yarn-error.log* # Visual Studio Code .vscode/ -.env \ No newline at end of file +.env + +# Documentation +docs \ No newline at end of file From 1281ae0d97e9415ac92948ef04878634b08f866a Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 6 Apr 2022 11:38:04 +0200 Subject: [PATCH 243/536] better overflow of text in projectcard --- backend/migrations/versions/c5bdaa5815ca_.py | 24 ++++++++++++++++ .../ProjectCard/ProjectCard.tsx | 19 +++++++++---- .../ProjectsComponents/ProjectCard/styles.ts | 10 +++++++ .../src/views/ProjectsPage/ProjectsPage.tsx | 28 ++++++------------- 4 files changed, 56 insertions(+), 25 deletions(-) create mode 100644 backend/migrations/versions/c5bdaa5815ca_.py diff --git a/backend/migrations/versions/c5bdaa5815ca_.py b/backend/migrations/versions/c5bdaa5815ca_.py new file mode 100644 index 000000000..6de85ba03 --- /dev/null +++ b/backend/migrations/versions/c5bdaa5815ca_.py @@ -0,0 +1,24 @@ +"""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/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 4772f57dc..5d490be5a 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -1,4 +1,12 @@ -import { CardContainer, CoachesContainer, CoachContainer, Delete, TitleContainer } from "./styles"; +import { + CardContainer, + CoachesContainer, + CoachContainer, + Delete, + TitleContainer, + Title, + Client +} from "./styles"; export default function ProjectCard({ name, @@ -12,14 +20,13 @@ export default function ProjectCard({ return ( -
                                        -

                                        {name}

                                        -
                                        - + + {name} + X
                                        -

                                        {client}

                                        + {client} {coaches.map((element, _index) => ( {element} diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts index 86bf7cc25..9c30ca7b0 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts +++ b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts @@ -15,6 +15,16 @@ export const TitleContainer = styled.div` justify-content: space-between; `; +export const Title = styled.h2` + text-overflow: ellipsis; + overflow: hidden; +` + +export const Client = styled.h3` + text-overflow: ellipsis; + overflow: hidden; +` + export const CoachesContainer = styled.div` display: flex; margin-top: 20px; diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index 69d663440..35b2030b3 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -2,13 +2,17 @@ import React, { useEffect, useState } from "react"; import { getProjects } from "../../utils/api/projects"; import "./ProjectsPage.css"; -import { ProjectCard } from "../../components/ProjectsComponents"; +import {ProjectCard} from "../../components/ProjectsComponents"; import { CardsGrid } from "./styles"; +interface Partner { + name: string +} + interface Project { name: string; - partners: any[]; + partners: Partner[]; } function ProjectPage() { @@ -17,7 +21,7 @@ function ProjectPage() { useEffect(() => { async function callProjects() { - const response = await getProjects("1"); + const response = await getProjects("summerof2022"); if (response) { setGotProjects(true); setProjects(response.projects); @@ -41,25 +45,11 @@ function ProjectPage() { ))} - - - +
                                        ); From b89ef9b3ed91d128671d8a43fb9c3fe0e891976f Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 6 Apr 2022 12:04:54 +0200 Subject: [PATCH 244/536] display number of students in a project --- .../ProjectCard/ProjectCard.tsx | 21 ++++++++++++++----- .../ProjectsComponents/ProjectCard/styles.ts | 15 ++++++++++--- .../src/views/ProjectsPage/ProjectsPage.tsx | 7 ++++--- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 5d490be5a..4f6a159bd 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -2,31 +2,42 @@ import { CardContainer, CoachesContainer, CoachContainer, + NumberOfStudents, Delete, TitleContainer, Title, - Client + Client, } from "./styles"; +import { BsPersonFill } from "react-icons/bs"; + export default function ProjectCard({ name, client, + numberOfStudents, coaches, }: { name: string; client: string; + numberOfStudents: number; coaches: string[]; }) { return ( - - {name} - + {name} X - {client} + + {client} + + {numberOfStudents} + + + + + {coaches.map((element, _index) => ( {element} diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts index 9c30ca7b0..f5f8cc7d2 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts +++ b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts @@ -18,12 +18,21 @@ export const TitleContainer = styled.div` export const Title = styled.h2` text-overflow: ellipsis; overflow: hidden; -` +`; -export const Client = styled.h3` +export const Client = styled.h5` + display: flex; + align-items: center; + color: lightgray; text-overflow: ellipsis; overflow: hidden; -` +`; + +export const NumberOfStudents = styled.div` + margin-left: auto; + display: flex; + align-items: center; +`; export const CoachesContainer = styled.div` display: flex; diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index 35b2030b3..9d7a75c24 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -2,16 +2,17 @@ import React, { useEffect, useState } from "react"; import { getProjects } from "../../utils/api/projects"; import "./ProjectsPage.css"; -import {ProjectCard} from "../../components/ProjectsComponents"; +import { ProjectCard } from "../../components/ProjectsComponents"; import { CardsGrid } from "./styles"; interface Partner { - name: string + name: string; } interface Project { name: string; + numberOfStudents: number; partners: Partner[]; } @@ -45,11 +46,11 @@ function ProjectPage() { ))} -
                                        ); From 52c9b3849aae9095b5bdcb624618282d95dbb3f7 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Wed, 6 Apr 2022 14:49:32 +0200 Subject: [PATCH 245/536] Refactor AdminsPage --- .../components/AdminsComponents/AddAdmin.tsx | 102 +++++++ .../components/AdminsComponents/AdminList.tsx | 56 ++++ .../AdminsComponents/AdminListItem.tsx | 20 ++ .../AdminsComponents/RemoveAdmin.tsx | 92 ++++++ .../src/components/AdminsComponents/index.ts | 4 + .../src/components/AdminsComponents/styles.ts | 24 ++ .../Coaches/CoachesComponents/AddCoach.tsx | 6 +- frontend/src/components/index.ts | 1 + .../src/views/AdminsPage/Admins/Admins.tsx | 262 +----------------- .../src/views/AdminsPage/Admins/styles.ts | 23 -- 10 files changed, 308 insertions(+), 282 deletions(-) create mode 100644 frontend/src/components/AdminsComponents/AddAdmin.tsx create mode 100644 frontend/src/components/AdminsComponents/AdminList.tsx create mode 100644 frontend/src/components/AdminsComponents/AdminListItem.tsx create mode 100644 frontend/src/components/AdminsComponents/RemoveAdmin.tsx create mode 100644 frontend/src/components/AdminsComponents/index.ts create mode 100644 frontend/src/components/AdminsComponents/styles.ts diff --git a/frontend/src/components/AdminsComponents/AddAdmin.tsx b/frontend/src/components/AdminsComponents/AddAdmin.tsx new file mode 100644 index 000000000..38ef18c72 --- /dev/null +++ b/frontend/src/components/AdminsComponents/AddAdmin.tsx @@ -0,0 +1,102 @@ +import { User } from "../../utils/api/users/users"; +import React, { useState } from "react"; +import { addAdmin } from "../../utils/api/users/admins"; +import { AddAdminButton, ModalContentConfirm, Warning } from "./styles"; +import { Button, Modal } from "react-bootstrap"; +import { Typeahead } from "react-bootstrap-typeahead"; +import { Error } from "../UsersComponents/PendingRequests/styles"; + +/** + * 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; +} + +/** + * Button and popup to add an existing user as admin. + * @param props.users All users which can be added as admin. + * @param props.refresh A function which is called when a new admin is added. + */ +export default function AddAdmin(props: { users: User[]; refresh: () => void }) { + const [show, setShow] = useState(false); + const [selected, setSelected] = useState(undefined); + const [error, setError] = useState(""); + + const handleClose = () => { + setSelected(undefined); + setShow(false); + }; + const handleShow = () => { + setShow(true); + setError(""); + }; + + async function addUserAsAdmin(userId: number) { + try { + const added = await addAdmin(userId); + if (added) { + props.refresh(); + handleClose(); + } else { + setError("Something went wrong. Failed to add admin"); + } + } catch (error) { + setError("Something went wrong. Failed to add admin"); + } + } + + return ( + <> + + Add admin + + + + + + Add Admin + + + { + setSelected(selected[0] as User); + }} + id="non-admin-users" + options={props.users} + labelKey="name" + emptyLabel="No users found." + placeholder={"name"} + /> + + + + + + {error} + + + + + ); +} diff --git a/frontend/src/components/AdminsComponents/AdminList.tsx b/frontend/src/components/AdminsComponents/AdminList.tsx new file mode 100644 index 000000000..85c13c267 --- /dev/null +++ b/frontend/src/components/AdminsComponents/AdminList.tsx @@ -0,0 +1,56 @@ +import { User } from "../../utils/api/users/users"; +import { SpinnerContainer } from "../UsersComponents/PendingRequests/styles"; +import { Spinner } from "react-bootstrap"; +import { AdminsTable } from "./styles"; +import React from "react"; +import { AdminListItem } from "./index"; + +/** + * 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. + * @constructor + */ +export default function AdminList(props: { + admins: User[]; + loading: boolean; + gotData: boolean; + refresh: () => 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} + + ); +} diff --git a/frontend/src/components/AdminsComponents/AdminListItem.tsx b/frontend/src/components/AdminsComponents/AdminListItem.tsx new file mode 100644 index 000000000..90997a5db --- /dev/null +++ b/frontend/src/components/AdminsComponents/AdminListItem.tsx @@ -0,0 +1,20 @@ +import { User } from "../../utils/api/users/users"; +import React from "react"; +import { RemoveAdmin } from "./index"; + +/** + * 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. + */ +export default function AdminItem(props: { admin: User; refresh: () => void }) { + return ( + + {props.admin.name} + {props.admin.email} + + + + + ); +} diff --git a/frontend/src/components/AdminsComponents/RemoveAdmin.tsx b/frontend/src/components/AdminsComponents/RemoveAdmin.tsx new file mode 100644 index 000000000..fdb14b6f0 --- /dev/null +++ b/frontend/src/components/AdminsComponents/RemoveAdmin.tsx @@ -0,0 +1,92 @@ +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/PendingRequests/styles"; + +/** + * 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. + */ +export default function RemoveAdmin(props: { admin: User; refresh: () => 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"); + } + } + + return ( + <> + + + + + + Remove Admin + + +

                                        {props.admin.name}

                                        +

                                        {props.admin.email}

                                        +

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

                                        +
                                        + + + + + {error} + +
                                        +
                                        + + ); +} diff --git a/frontend/src/components/AdminsComponents/index.ts b/frontend/src/components/AdminsComponents/index.ts new file mode 100644 index 000000000..7b7bf52bc --- /dev/null +++ b/frontend/src/components/AdminsComponents/index.ts @@ -0,0 +1,4 @@ +export { default as AddAdmin } from "./AddAdmin"; +export { default as AdminList } from "./AdminList"; +export { default as AdminListItem } from "./AdminListItem"; +export { default as RemoveAdmin } from "./RemoveAdmin"; diff --git a/frontend/src/components/AdminsComponents/styles.ts b/frontend/src/components/AdminsComponents/styles.ts new file mode 100644 index 000000000..394500255 --- /dev/null +++ b/frontend/src/components/AdminsComponents/styles.ts @@ -0,0 +1,24 @@ +import styled from "styled-components"; +import { Button, Table } from "react-bootstrap"; + +export const Warning = styled.div` + color: var(--osoc_red); +`; + +export const AdminsTable = styled(Table)``; + +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); +`; + +export const AddAdminButton = styled(Button).attrs({ + size: "sm", +})` + float: right; +`; diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx index 828704b7a..4fa804997 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx @@ -1,10 +1,10 @@ import { User } from "../../../../utils/api/users/users"; import React, { useState } from "react"; import { addCoachToEdition } from "../../../../utils/api/users/coaches"; -import { AddAdminButton, ModalContentGreen } from "../../../../views/AdminsPage/Admins/styles"; import { Button, Modal } from "react-bootstrap"; import { Typeahead } from "react-bootstrap-typeahead"; import { Error } from "../../PendingRequests/styles"; +import { AddAdminButton, ModalContentConfirm } from "../../../AdminsComponents/styles"; /** * A button and popup to add a new coach to the given edition. @@ -45,7 +45,7 @@ export default function AddCoach(props: { users: User[]; edition: string; refres - + Add Coach @@ -79,7 +79,7 @@ export default function AddCoach(props: { users: User[]; edition: string; refres {error} - + ); diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index b510a6011..0d8fcf01f 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -6,3 +6,4 @@ export { default as OSOCLetters } from "./OSOCLetters"; export { default as PrivateRoute } from "./PrivateRoute"; export * as RegisterComponents from "./RegisterComponents"; export * as UsersComponents from "./UsersComponents"; +export * as AdminsComponents from "./AdminsComponents"; diff --git a/frontend/src/views/AdminsPage/Admins/Admins.tsx b/frontend/src/views/AdminsPage/Admins/Admins.tsx index fa2023f46..7182ec0bf 100644 --- a/frontend/src/views/AdminsPage/Admins/Admins.tsx +++ b/frontend/src/views/AdminsPage/Admins/Admins.tsx @@ -1,255 +1,9 @@ import React, { useEffect, useState } from "react"; -import { - AdminsContainer, - AdminsTable, - ModalContentGreen, - ModalContentRed, - AddAdminButton, - Warning, -} from "./styles"; +import { AdminsContainer } from "./styles"; import { getUsers, User } from "../../../utils/api/users/users"; -import { Button, Modal, Spinner } from "react-bootstrap"; -import { - addAdmin, - getAdmins, - removeAdmin, - removeAdminAndCoach, -} from "../../../utils/api/users/admins"; -import { - Error, - SearchInput, - SpinnerContainer, -} from "../../../components/UsersComponents/PendingRequests/styles"; -import { Typeahead } from "react-bootstrap-typeahead"; - -function AdminFilter(props: { - search: boolean; - searchTerm: string; - filter: (key: string) => void; -}) { - return props.filter(e.target.value)} />; -} - -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; -} - -function AddAdmin(props: { users: User[]; refresh: () => void }) { - const [show, setShow] = useState(false); - const [selected, setSelected] = useState(undefined); - const [error, setError] = useState(""); - - const handleClose = () => { - setSelected(undefined); - setShow(false); - }; - const handleShow = () => { - setShow(true); - setError(""); - }; - - async function addUserAsAdmin(userId: number) { - try { - const added = await addAdmin(userId); - if (added) { - props.refresh(); - handleClose(); - } else { - setError("Something went wrong. Failed to add admin"); - } - } catch (error) { - setError("Something went wrong. Failed to add admin"); - } - } - - return ( - <> - - Add admin - - - - - - Add Admin - - - { - setSelected(selected[0] as User); - }} - id="non-admin-users" - options={props.users} - labelKey="name" - emptyLabel="No users found." - placeholder={"name"} - /> - - - - - - {error} - - - - - ); -} - -function RemoveAdmin(props: { admin: User; refresh: () => 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"); - } - } - - return ( - <> - - - - - - Remove Admin - - -

                                        {props.admin.name}

                                        -

                                        {props.admin.email}

                                        -

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

                                        -
                                        - - - - - {error} - -
                                        -
                                        - - ); -} - -function AdminItem(props: { admin: User; refresh: () => void }) { - return ( - - {props.admin.name} - {props.admin.email} - - - - - ); -} - -function AdminsList(props: { - admins: User[]; - loading: boolean; - gotData: boolean; - refresh: () => 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} - - ); -} +import { getAdmins } from "../../../utils/api/users/admins"; +import { Error, SearchInput } from "../../../components/UsersComponents/PendingRequests/styles"; +import { AddAdmin, AdminList } from "../../../components/AdminsComponents"; export default function Admins() { const [allAdmins, setAllAdmins] = useState([]); @@ -304,13 +58,9 @@ export default function Admins() { return ( - 0} - searchTerm={searchTerm} - filter={word => filter(word)} - /> + filter(e.target.value)} /> - + {error} ); diff --git a/frontend/src/views/AdminsPage/Admins/styles.ts b/frontend/src/views/AdminsPage/Admins/styles.ts index 3f0534424..be7039a25 100644 --- a/frontend/src/views/AdminsPage/Admins/styles.ts +++ b/frontend/src/views/AdminsPage/Admins/styles.ts @@ -1,30 +1,7 @@ import styled from "styled-components"; -import { Button, Table } from "react-bootstrap"; export const AdminsContainer = styled.div` width: 50%; min-width: 600px; margin: 10px auto auto; `; - -export const AdminsTable = styled(Table)``; - -export const ModalContentGreen = styled.div` - border: 3px solid var(--osoc_green); - background-color: var(--osoc_blue); -`; - -export const ModalContentRed = styled.div` - border: 3px solid var(--osoc_red); - background-color: var(--osoc_blue); -`; - -export const AddAdminButton = styled(Button).attrs({ - size: "sm", -})` - float: right; -`; - -export const Warning = styled.div` - color: var(--osoc_red); -`; From 9c712864acdd39d84a6e6ff59ef370982ef7068c Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 6 Apr 2022 15:02:10 +0200 Subject: [PATCH 246/536] coaches overflow-x --- .../ProjectCard/ProjectCard.tsx | 14 +++++---- .../ProjectsComponents/ProjectCard/styles.ts | 29 ++++++++++++------- .../src/views/ProjectsPage/ProjectsPage.tsx | 2 +- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 4f6a159bd..bb180faf0 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -2,10 +2,12 @@ import { CardContainer, CoachesContainer, CoachContainer, + CoachText, NumberOfStudents, Delete, TitleContainer, Title, + ClientContainer, Client, } from "./styles"; @@ -28,19 +30,19 @@ export default function ProjectCard({ {name} X - - - {client} + + {client} {numberOfStudents} - - + {coaches.map((element, _index) => ( - {element} + + {element} + ))} diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts index f5f8cc7d2..c9a9b2597 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts +++ b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts @@ -3,8 +3,7 @@ import styled from "styled-components"; export const CardContainer = styled.div` border: 2px solid #1a1a36; border-radius: 20px; - margin: 20px; - margin-bottom: 5px; + margin: 10px 20px; padding: 20px 20px 20px 20px; background-color: #323252; box-shadow: 5px 5px 15px #131329; @@ -20,23 +19,29 @@ export const Title = styled.h2` overflow: hidden; `; -export const Client = styled.h5` +export const ClientContainer = styled.div` display: flex; - align-items: center; + align-items: top; + justify-content: space-between; color: lightgray; +`; + +export const Client = styled.h5` text-overflow: ellipsis; overflow: hidden; `; export const NumberOfStudents = styled.div` - margin-left: auto; + margin-left: 2.5%; display: flex; align-items: center; + margin-bottom: 4px; `; export const CoachesContainer = styled.div` display: flex; margin-top: 20px; + overflow-x: scroll; `; export const CoachContainer = styled.div` @@ -44,13 +49,17 @@ export const CoachContainer = styled.div` border-radius: 10px; margin-right: 10px; text-align: center; - padding: 10px; - max-width: 50%; - min-width: 20%; - text-overflow: ellipsis; - overflow: hidden; + padding: 10px 20px; + width: fit-content; + max-width: 20vw; `; +export const CoachText = styled.div` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +` + export const Delete = styled.button` background-color: #f14a3b; padding: 5px 10px; diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index 9d7a75c24..36c4df9d4 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -47,7 +47,7 @@ function ProjectPage() { name={project.name} client={project.partners[0].name} numberOfStudents={project.numberOfStudents} - coaches={["Langemietnaamdielangis", "Bart"]} + coaches={["Langemietnaamdielangis", "Bart met een lange naam", "dfjdf", "kdjfdif", "kfjdif"]} key={_index} /> ))} From 804af11cde0dc73fc523756566fa446424396a8d Mon Sep 17 00:00:00 2001 From: stijndcl Date: Wed, 6 Apr 2022 15:45:58 +0200 Subject: [PATCH 247/536] Create router structure --- frontend/src/Router.tsx | 47 ++++++++++++++++--- .../components/PrivateRoute/PrivateRoute.tsx | 1 - frontend/src/views/LoginPage/LoginPage.tsx | 5 +- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 8bb4d2dd7..d8455065c 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -3,7 +3,7 @@ import VerifyingTokenPage from "./views/VerifyingTokenPage"; import LoginPage from "./views/LoginPage"; import { Container, ContentWrapper } from "./app.styles"; import NavBar from "./components/navbar"; -import { BrowserRouter, Route, Routes } from "react-router-dom"; +import { BrowserRouter, Navigate, Outlet, Route, Routes } from "react-router-dom"; import RegisterPage from "./views/RegisterPage"; import StudentsPage from "./views/StudentsPage/StudentsPage"; import UsersPage from "./views/UsersPage"; @@ -38,16 +38,49 @@ export default function Router() { } /> } /> - }> - } /> + }> + {/* TODO admins page */} + } /> - }> - } /> + }> + {/* TODO editions page */} + } /> + }> + {/* TODO create edition page */} + } /> + + }> + {/* TODO edition page? do we need? maybe just some nav/links? */} + } /> + + {/* Projects routes */} + }> + } /> + }> + {/* TODO create project page */} + } /> + + {/* TODO project page */} + } /> + + + {/* Students routes */} + } /> + {/* TODO student page */} + } /> + {/* TODO student emails page */} + } /> + + {/* Users routes */} + }> + } /> + + - } /> } /> } /> - } /> + } /> + } /> )} diff --git a/frontend/src/components/PrivateRoute/PrivateRoute.tsx b/frontend/src/components/PrivateRoute/PrivateRoute.tsx index b61b34400..0eed20a03 100644 --- a/frontend/src/components/PrivateRoute/PrivateRoute.tsx +++ b/frontend/src/components/PrivateRoute/PrivateRoute.tsx @@ -16,6 +16,5 @@ import { Navigate, Outlet } from "react-router-dom"; */ export default function PrivateRoute() { const { isLoggedIn } = useAuth(); - // TODO check edition existence & access once routes have been moved over return isLoggedIn ? : ; } diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index abde13a18..610e5aebc 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -28,10 +28,7 @@ export default function LoginPage() { // If the user is already logged in, redirect them to // the "students" page instead of showing the login page if (authCtx.isLoggedIn) { - // TODO find other homepage to go to - // perhaps editions? - // (the rest requires an edition) - navigate("/students"); + navigate("/editions"); } }, [authCtx.isLoggedIn, navigate]); From 6bdbb643db5cadca848cca54e9f6f17ad02be715 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Wed, 6 Apr 2022 15:49:02 +0200 Subject: [PATCH 248/536] Redirect /login to / --- frontend/src/Router.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index d8455065c..5bfc27e79 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -37,6 +37,8 @@ export default function Router() { // the LoginPage } /> + {/* Redirect /login to the login page */} + } /> } /> }> {/* TODO admins page */} From 2479789844ecb5d68068036c53614de34b5a94fe Mon Sep 17 00:00:00 2001 From: stijndcl Date: Wed, 6 Apr 2022 15:55:37 +0200 Subject: [PATCH 249/536] Replace history --- frontend/src/Router.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 5bfc27e79..53ce61f27 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -82,7 +82,7 @@ export default function Router() { } /> } /> } /> - } /> + } /> )} From 8a27f276591a77d237f06869123a1969a0da0e47 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 6 Apr 2022 16:53:17 +0200 Subject: [PATCH 250/536] Started working on a popup for delete --- .../ProjectCard/ProjectCard.tsx | 31 ++++++++++++++++++- .../ProjectsComponents/ProjectCard/styles.ts | 12 +++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index bb180faf0..378915827 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -12,6 +12,10 @@ import { } from "./styles"; import { BsPersonFill } from "react-icons/bs"; +import { TiDeleteOutline } from "react-icons/ti"; + +import { Modal, Button } from "react-bootstrap"; +import { useState } from "react"; export default function ProjectCard({ name, @@ -24,11 +28,36 @@ export default function ProjectCard({ numberOfStudents: number; coaches: string[]; }) { + const [show, setShow] = useState(false); + const handleClose = () => setShow(false); + const handleShow = () => setShow(true); + return ( {name} - X + + + + + <> + + + Confirm delete + + + Are you sure you want to delete {name}? + + + + + + + {client} diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts index c9a9b2597..383250d3e 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts +++ b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts @@ -1,3 +1,4 @@ +import { Modal } from "react-bootstrap"; import styled from "styled-components"; export const CardContainer = styled.div` @@ -11,6 +12,7 @@ export const CardContainer = styled.div` export const TitleContainer = styled.div` display: flex; + align-items: baseline; justify-content: space-between; `; @@ -62,9 +64,15 @@ export const CoachText = styled.div` export const Delete = styled.button` background-color: #f14a3b; - padding: 5px 10px; + padding: 5px 5px; border: 0; border-radius: 5px; - max-height: 35px; + max-height: 30px; margin-left: 5%; + display: flex; + align-items: center; `; + +export const PopUp = styled(Modal)` + +` From 8af56fe845f027306215504e41c7d4ab97505233 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Wed, 6 Apr 2022 17:07:07 +0200 Subject: [PATCH 251/536] Begin --- backend/src/app/logic/users.py | 7 ++++-- backend/src/app/schemas/users.py | 24 +++++++++++++++++++ backend/src/database/crud/users.py | 4 ++-- .../test_database/test_crud/test_users.py | 7 +++++- .../test_routers/test_users/test_users.py | 6 +++++ 5 files changed, 43 insertions(+), 5 deletions(-) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index 47308884c..35dd8d888 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session -from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, UserRequest +from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, UserRequest, user_model_to_schema import src.database.crud.users as users_crud from src.database.models import User @@ -22,7 +22,10 @@ def get_users_list(db: Session, admin: bool, edition_name: str | None) -> UsersL else: users_orm = users_crud.get_users_from_edition(db, edition_name) - return UsersListResponse(users=users_orm) + users = [] + for user in users_orm: + users.append(user_model_to_schema(user)) + return UsersListResponse(users=users) def get_user_editions(user: User) -> list[str]: diff --git a/backend/src/app/schemas/users.py b/backend/src/app/schemas/users.py index 8b6f0bf30..81fcdf635 100644 --- a/backend/src/app/schemas/users.py +++ b/backend/src/app/schemas/users.py @@ -1,4 +1,5 @@ from src.app.schemas.utils import CamelCaseModel +from src.database.models import User as ModelUser class User(CamelCaseModel): @@ -7,12 +8,35 @@ class User(CamelCaseModel): user_id: int name: str admin: bool + auth_type: str | None + email: str | None class Config: """Set to ORM mode""" orm_mode = True +def user_model_to_schema(model_user: ModelUser) -> User: + auth_type: str | None = None + email: str | None = None + if model_user.email_auth is not None: + auth_type = "email" + email = model_user.email_auth.email + elif model_user.github_auth is not None: + auth_type = "github" + email = model_user.github_auth.email + elif model_user.google_auth is not None: + auth_type = "google" + email = model_user.google_auth.email + + return User( + user_id=model_user.user_id, + name=model_user.name, + admin=model_user.admin, + auth_type=auth_type, + email=email) + + class UsersListResponse(CamelCaseModel): """Model for a list of users""" diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index bc8ffa66a..97062ce29 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -1,5 +1,5 @@ from sqlalchemy.orm import Session -from src.database.models import user_editions, User, Edition, CoachRequest +from src.database.models import user_editions, User, Edition, CoachRequest, AuthGoogle, AuthEmail, AuthGitHub def get_all_admins(db: Session) -> list[User]: @@ -7,7 +7,7 @@ def get_all_admins(db: Session) -> list[User]: Get all admins """ - return db.query(User).where(User.admin).all() + return db.query(User).where(User.admin).join(AuthEmail, isouter=True).join(AuthGitHub, isouter=True).join(AuthGoogle, isouter=True).all() def get_all_users(db: Session) -> list[User]: diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index d3f428d91..71314933b 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -12,7 +12,6 @@ def data(database_session: Session) -> dict[str, str]: # Create users user1 = models.User(name="user1", admin=True) - database_session.add(user1) user2 = models.User(name="user2", admin=False) database_session.add(user2) @@ -25,6 +24,12 @@ def data(database_session: Session) -> dict[str, str]: 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") + database_session.add(email_auth1) + database_session.add(github_auth1) + database_session.commit() + # Create coach roles database_session.execute(models.user_editions.insert(), [ {"user_id": user1.user_id, "edition_id": edition1.edition_id}, diff --git a/backend/tests/test_routers/test_users/test_users.py b/backend/tests/test_routers/test_users/test_users.py index 6c0563fe8..317eacd13 100644 --- a/backend/tests/test_routers/test_users/test_users.py +++ b/backend/tests/test_routers/test_users/test_users.py @@ -28,6 +28,12 @@ def data(database_session: Session) -> dict[str, str | int]: 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") + database_session.add(email_auth1) + database_session.add(github_auth1) + database_session.commit() + # Create coach roles database_session.execute(models.user_editions.insert(), [ {"user_id": user1.user_id, "edition_id": edition1.edition_id}, From 53d3d6eb972f65d836d4712367d0c549bf6ee57e Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Wed, 6 Apr 2022 17:59:39 +0200 Subject: [PATCH 252/536] Edit DB models + add test --- ...7b881db_add_email_to_google_github_auth.py | 42 +++++++++++++++++++ backend/src/database/models.py | 2 + .../test_database/test_crud/test_users.py | 1 + .../test_routers/test_users/test_users.py | 13 ++++++ 4 files changed, 58 insertions(+) create mode 100644 backend/migrations/versions/a4a047b881db_add_email_to_google_github_auth.py 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 new file mode 100644 index 000000000..f293ca85c --- /dev/null +++ b/backend/migrations/versions/a4a047b881db_add_email_to_google_github_auth.py @@ -0,0 +1,42 @@ +"""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/src/database/models.py b/backend/src/database/models.py index ab9975d1e..1890b2a76 100644 --- a/backend/src/database/models.py +++ b/backend/src/database/models.py @@ -40,6 +40,7 @@ class AuthGitHub(Base): gh_auth_id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.user_id"), nullable=False) + email = Column(Text, unique=True, nullable=False) user: User = relationship("User", back_populates="github_auth", uselist=False) @@ -50,6 +51,7 @@ class AuthGoogle(Base): google_auth_id = Column(Integer, primary_key=True) 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) diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index 71314933b..660af7479 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -41,6 +41,7 @@ def data(database_session: Session) -> dict[str, str]: "user2": user2.user_id, "edition1": edition1.name, "edition2": edition2.name, + "email1": "user1@mail.com" } diff --git a/backend/tests/test_routers/test_users/test_users.py b/backend/tests/test_routers/test_users/test_users.py index 317eacd13..a0962ec66 100644 --- a/backend/tests/test_routers/test_users/test_users.py +++ b/backend/tests/test_routers/test_users/test_users.py @@ -45,6 +45,8 @@ def data(database_session: Session) -> dict[str, str | int]: "user2": user2.user_id, "edition1": edition1.name, "edition2": edition2.name, + "email1": email_auth1.email, + "email2": github_auth1.email } @@ -61,6 +63,17 @@ def test_get_all_users(database_session: Session, auth_client: AuthClient, data: assert data["user2"] in user_ids +def test_get_users_response(database_session: Session, 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["email"] == data["email1"] + user2 = [user for user in users if user["userId"] == data["user2"]][0] + assert user2["email"] == data["email2"] + + def test_get_all_admins(database_session: Session, auth_client: AuthClient, data: dict[str, str | int]): """Test endpoint for getting a list of admins""" auth_client.admin() From be6f68e7b0589a1ab3c9fc0b6e64b5979200a32e Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Wed, 6 Apr 2022 18:06:31 +0200 Subject: [PATCH 253/536] Linting --- backend/src/app/schemas/users.py | 1 + backend/src/database/crud/users.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/src/app/schemas/users.py b/backend/src/app/schemas/users.py index 81fcdf635..a301134b8 100644 --- a/backend/src/app/schemas/users.py +++ b/backend/src/app/schemas/users.py @@ -17,6 +17,7 @@ class Config: def user_model_to_schema(model_user: ModelUser) -> User: + """Create User Schema from User Model""" auth_type: str | None = None email: str | None = None if model_user.email_auth is not None: diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 97062ce29..8f16dcf5a 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -7,7 +7,12 @@ def get_all_admins(db: Session) -> list[User]: Get all admins """ - return db.query(User).where(User.admin).join(AuthEmail, isouter=True).join(AuthGitHub, isouter=True).join(AuthGoogle, isouter=True).all() + return db.query(User)\ + .where(User.admin)\ + .join(AuthEmail, isouter=True)\ + .join(AuthGitHub, isouter=True)\ + .join(AuthGoogle, isouter=True)\ + .all() def get_all_users(db: Session) -> list[User]: From fb12c16c6123d6c69fdbd4e36090f609b3f91ed6 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 6 Apr 2022 18:43:57 +0200 Subject: [PATCH 254/536] added confirmation modal to delete project --- .../ProjectsComponents/Modal/Modal.tsx | 26 +++++++++++++++ .../ProjectsComponents/Modal/index.ts | 1 + .../ProjectsComponents/Modal/styles.ts | 33 +++++++++++++++++++ .../ProjectCard/ProjectCard.tsx | 23 ++----------- .../ProjectsComponents/ProjectCard/styles.ts | 1 + frontend/src/components/index.ts | 1 + 6 files changed, 65 insertions(+), 20 deletions(-) create mode 100644 frontend/src/components/ProjectsComponents/Modal/Modal.tsx create mode 100644 frontend/src/components/ProjectsComponents/Modal/index.ts create mode 100644 frontend/src/components/ProjectsComponents/Modal/styles.ts diff --git a/frontend/src/components/ProjectsComponents/Modal/Modal.tsx b/frontend/src/components/ProjectsComponents/Modal/Modal.tsx new file mode 100644 index 000000000..d1cc25ac8 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/Modal/Modal.tsx @@ -0,0 +1,26 @@ +import { StyledModal, Button, ModalFooter, ModalHeader, DeleteButton } from "./styles"; + +export default function Modal({ + show, + handleClose, + name, +}: { + show: boolean; + handleClose: () => void; + name: string; +}) { + return ( + + + Confirm delete + + + Are you sure you want to delete {name}? + + + + Delete + + + ); +} diff --git a/frontend/src/components/ProjectsComponents/Modal/index.ts b/frontend/src/components/ProjectsComponents/Modal/index.ts new file mode 100644 index 000000000..09b91f72b --- /dev/null +++ b/frontend/src/components/ProjectsComponents/Modal/index.ts @@ -0,0 +1 @@ +export { default } from "./Modal"; diff --git a/frontend/src/components/ProjectsComponents/Modal/styles.ts b/frontend/src/components/ProjectsComponents/Modal/styles.ts new file mode 100644 index 000000000..3d06faafd --- /dev/null +++ b/frontend/src/components/ProjectsComponents/Modal/styles.ts @@ -0,0 +1,33 @@ +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: 10px; + border-color: #f14a3b; + } +`; + +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; +`; + + +export const DeleteButton = styled(Button)` + background-color: #f14a3b; +`; diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 378915827..9450d2e1e 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -14,9 +14,10 @@ import { import { BsPersonFill } from "react-icons/bs"; import { TiDeleteOutline } from "react-icons/ti"; -import { Modal, Button } from "react-bootstrap"; import { useState } from "react"; +import Modal from "../Modal" + export default function ProjectCard({ name, client, @@ -39,25 +40,7 @@ export default function ProjectCard({ - - <> - - - Confirm delete - - - Are you sure you want to delete {name}? - - - - - - - + {client} diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts index 383250d3e..eae0656d7 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts +++ b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts @@ -76,3 +76,4 @@ export const Delete = styled.button` export const PopUp = styled(Modal)` ` + diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index ffe2ef414..e081404f2 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -5,3 +5,4 @@ export { default as NavBar } from "./navbar"; export { default as OSOCLetters } from "./OSOCLetters"; export { default as PrivateRoute } from "./PrivateRoute"; export * as RegisterComponents from "./RegisterComponents"; +export { default as Modal } from "./ProjectsComponents/Modal" From 57c7fe15edca97b99cdf8f05aee9c2f1c01a989c Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 6 Apr 2022 19:02:41 +0200 Subject: [PATCH 255/536] search and create button --- .../src/views/ProjectsPage/ProjectsPage.tsx | 8 +++--- frontend/src/views/ProjectsPage/styles.ts | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index 36c4df9d4..93d958fd4 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -4,7 +4,7 @@ import "./ProjectsPage.css"; import { ProjectCard } from "../../components/ProjectsComponents"; -import { CardsGrid } from "./styles"; +import { CardsGrid, CreateButton, SearchButton, SearchField } from "./styles"; interface Partner { name: string; @@ -36,9 +36,9 @@ function ProjectPage() { return (
                                        - - - + + Search + Create Project
                                        diff --git a/frontend/src/views/ProjectsPage/styles.ts b/frontend/src/views/ProjectsPage/styles.ts index 3c93d10af..109fa4bd9 100644 --- a/frontend/src/views/ProjectsPage/styles.ts +++ b/frontend/src/views/ProjectsPage/styles.ts @@ -6,3 +6,29 @@ export const CardsGrid = styled.div` grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-auto-flow: dense; `; + +export const SearchField = styled.input` + margin: 20px 5px 20px 20px; + padding: 5px 10px; + background-color: #131329; + color: white; + border: none; + border-radius: 10px; +` + +export const SearchButton = styled.button` + padding: 5px 10px; + background-color: #00bfff; + color: white; + border: none; + border-radius: 10px; +` + +export const CreateButton = styled.button` + margin: 20px; + padding: 5px 10px; + background-color: #44dba4; + color: white; + border: none; + border-radius: 10px; +` From 14aed25bde34ae99282f460cc0650158a4b62445 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 6 Apr 2022 20:31:57 +0200 Subject: [PATCH 256/536] delete project now functional --- .../ConfirmDelete.tsx} | 14 ++++++----- .../ProjectsComponents/ConfirmDelete/index.ts | 1 + .../{Modal => ConfirmDelete}/styles.ts | 5 ++-- .../ProjectsComponents/Modal/index.ts | 1 - .../ProjectCard/ProjectCard.tsx | 24 +++++++++++++++++-- frontend/src/components/index.ts | 2 +- frontend/src/utils/api/projects.ts | 17 ++++++++++++- .../src/views/ProjectsPage/ProjectsPage.tsx | 17 ++++++++++--- 8 files changed, 64 insertions(+), 17 deletions(-) rename frontend/src/components/ProjectsComponents/{Modal/Modal.tsx => ConfirmDelete/ConfirmDelete.tsx} (60%) create mode 100644 frontend/src/components/ProjectsComponents/ConfirmDelete/index.ts rename frontend/src/components/ProjectsComponents/{Modal => ConfirmDelete}/styles.ts (99%) delete mode 100644 frontend/src/components/ProjectsComponents/Modal/index.ts diff --git a/frontend/src/components/ProjectsComponents/Modal/Modal.tsx b/frontend/src/components/ProjectsComponents/ConfirmDelete/ConfirmDelete.tsx similarity index 60% rename from frontend/src/components/ProjectsComponents/Modal/Modal.tsx rename to frontend/src/components/ProjectsComponents/ConfirmDelete/ConfirmDelete.tsx index d1cc25ac8..8c70148db 100644 --- a/frontend/src/components/ProjectsComponents/Modal/Modal.tsx +++ b/frontend/src/components/ProjectsComponents/ConfirmDelete/ConfirmDelete.tsx @@ -1,16 +1,18 @@ -import { StyledModal, Button, ModalFooter, ModalHeader, DeleteButton } from "./styles"; +import { StyledModal, ModalFooter, ModalHeader, Button, DeleteButton } from "./styles"; -export default function Modal({ - show, +export default function ConfirmDelete({ + visible, handleClose, + handleConfirm, name, }: { - show: boolean; + visible: boolean; + handleConfirm: () => void; handleClose: () => void; name: string; }) { return ( - + Confirm delete @@ -19,7 +21,7 @@ export default function Modal({ - Delete + Delete ); diff --git a/frontend/src/components/ProjectsComponents/ConfirmDelete/index.ts b/frontend/src/components/ProjectsComponents/ConfirmDelete/index.ts new file mode 100644 index 000000000..64a340b29 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/ConfirmDelete/index.ts @@ -0,0 +1 @@ +export { default } from "./ConfirmDelete"; diff --git a/frontend/src/components/ProjectsComponents/Modal/styles.ts b/frontend/src/components/ProjectsComponents/ConfirmDelete/styles.ts similarity index 99% rename from frontend/src/components/ProjectsComponents/Modal/styles.ts rename to frontend/src/components/ProjectsComponents/ConfirmDelete/styles.ts index 3d06faafd..a2f3e12f7 100644 --- a/frontend/src/components/ProjectsComponents/Modal/styles.ts +++ b/frontend/src/components/ProjectsComponents/ConfirmDelete/styles.ts @@ -14,10 +14,10 @@ export const StyledModal = styled(Modal)` 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; @@ -27,7 +27,6 @@ export const Button = styled.button` color: white; `; - export const DeleteButton = styled(Button)` background-color: #f14a3b; `; diff --git a/frontend/src/components/ProjectsComponents/Modal/index.ts b/frontend/src/components/ProjectsComponents/Modal/index.ts deleted file mode 100644 index 09b91f72b..000000000 --- a/frontend/src/components/ProjectsComponents/Modal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./Modal"; diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 9450d2e1e..a95022c2e 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -16,32 +16,52 @@ import { TiDeleteOutline } from "react-icons/ti"; import { useState } from "react"; -import Modal from "../Modal" +import ConfirmDelete from "../ConfirmDelete"; +import { deleteProject } from "../../../utils/api/projects"; export default function ProjectCard({ name, client, numberOfStudents, coaches, + edition, + id, + refreshEditions, }: { name: string; client: string; numberOfStudents: number; coaches: string[]; + edition: string; + id: string; + refreshEditions: () => void; }) { const [show, setShow] = useState(false); const handleClose = () => setShow(false); + const handleDelete = () => { + deleteProject(edition, id); + setShow(false); + refreshEditions(); + }; const handleShow = () => setShow(true); return ( {name} + - + + + {client} diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index e081404f2..3dfdc4179 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -5,4 +5,4 @@ export { default as NavBar } from "./navbar"; export { default as OSOCLetters } from "./OSOCLetters"; export { default as PrivateRoute } from "./PrivateRoute"; export * as RegisterComponents from "./RegisterComponents"; -export { default as Modal } from "./ProjectsComponents/Modal" +export { default as Modal } from "./ProjectsComponents/ConfirmDelete"; diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index 5c4c6982b..139398ab1 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -3,7 +3,7 @@ import { axiosInstance } from "./api"; export async function getProjects(edition: string) { try { - const response = await axiosInstance.get("/editions/" + edition + "/projects"); + const response = await axiosInstance.get("/editions/" + edition + "/projects/"); const projects = response.data; return projects; } catch (error) { @@ -14,3 +14,18 @@ export async function getProjects(edition: string) { } } } + +export async function deleteProject(edition: string, projectId: string) { + try { + const response = await axiosInstance.delete( + "/editions/" + edition + "/projects/" + projectId + ); + console.log(response); + } catch (error) { + if (axios.isAxiosError(error)) { + return false; + } else { + throw error; + } + } +} diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index 93d958fd4..cc362a76d 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -14,6 +14,8 @@ interface Project { name: string; numberOfStudents: number; partners: Partner[]; + editionName: string; + projectId: string; } function ProjectPage() { @@ -22,11 +24,11 @@ function ProjectPage() { useEffect(() => { async function callProjects() { + setGotProjects(true); const response = await getProjects("summerof2022"); if (response) { - setGotProjects(true); setProjects(response.projects); - } + } else setGotProjects(false); } if (!gotProjects) { callProjects(); @@ -47,7 +49,16 @@ function ProjectPage() { name={project.name} client={project.partners[0].name} numberOfStudents={project.numberOfStudents} - coaches={["Langemietnaamdielangis", "Bart met een lange naam", "dfjdf", "kdjfdif", "kfjdif"]} + coaches={[ + "Langemietnaamdielangis", + "Bart met een lange naam", + "dfjdf", + "kdjfdif", + "kfjdif", + ]} + edition={project.editionName} + id={project.projectId} + refreshEditions={() => setGotProjects(false)} key={_index} /> ))} From 83b73baec15242be7d8df672f55027bbea32e34f Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 6 Apr 2022 20:41:06 +0200 Subject: [PATCH 257/536] Show correct coaches --- .../ProjectsComponents/ProjectCard/ProjectCard.tsx | 8 ++++++-- frontend/src/views/ProjectsPage/ProjectsPage.tsx | 13 ++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index a95022c2e..828143795 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -19,6 +19,10 @@ import { useState } from "react"; import ConfirmDelete from "../ConfirmDelete"; import { deleteProject } from "../../../utils/api/projects"; +interface Coach { + name: string; +} + export default function ProjectCard({ name, client, @@ -31,7 +35,7 @@ export default function ProjectCard({ name: string; client: string; numberOfStudents: number; - coaches: string[]; + coaches: Coach[]; edition: string; id: string; refreshEditions: () => void; @@ -73,7 +77,7 @@ export default function ProjectCard({ {coaches.map((element, _index) => ( - {element} + {element.name} ))} diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index cc362a76d..01b921a18 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -10,10 +10,15 @@ interface Partner { name: string; } +interface Coach { + name: string; +} + interface Project { name: string; numberOfStudents: number; partners: Partner[]; + coaches: Coach[]; editionName: string; projectId: string; } @@ -49,13 +54,7 @@ function ProjectPage() { name={project.name} client={project.partners[0].name} numberOfStudents={project.numberOfStudents} - coaches={[ - "Langemietnaamdielangis", - "Bart met een lange naam", - "dfjdf", - "kdjfdif", - "kfjdif", - ]} + coaches={project.coaches} edition={project.editionName} id={project.projectId} refreshEditions={() => setGotProjects(false)} From f8a77127fcc1b1fbcd5b7e420847f4a63184248e Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 6 Apr 2022 22:17:27 +0200 Subject: [PATCH 258/536] filter on own projects (getting a bit ugly) --- frontend/src/contexts/auth-context.tsx | 7 +++ .../src/views/ProjectsPage/ProjectsPage.tsx | 63 +++++++++++++++++-- frontend/src/views/ProjectsPage/styles.ts | 4 ++ .../VerifyingTokenPage/VerifyingTokenPage.tsx | 1 + 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx index bdab830e2..e4ffb9780 100644 --- a/frontend/src/contexts/auth-context.tsx +++ b/frontend/src/contexts/auth-context.tsx @@ -11,6 +11,8 @@ export interface AuthContextState { setIsLoggedIn: (value: boolean | null) => void; role: Role | null; setRole: (value: Role | null) => void; + userId: number | null; + setUserId: (value: number | null) => void; token: string | null; setToken: (value: string | null) => void; editions: number[]; @@ -28,6 +30,8 @@ function authDefaultState(): AuthContextState { setIsLoggedIn: (_: boolean | null) => {}, role: null, setRole: (_: Role | null) => {}, + userId: null, + setUserId: (value: number | null) => {}, token: getToken(), setToken: (_: string | null) => {}, editions: [], @@ -55,6 +59,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { const [isLoggedIn, setIsLoggedIn] = useState(null); const [role, setRole] = useState(null); const [editions, setEditions] = useState([]); + const [userId, setUserId] = useState(null); // Default value: check LocalStorage const [token, setToken] = useState(getToken()); @@ -64,6 +69,8 @@ export function AuthProvider({ children }: { children: ReactNode }) { setIsLoggedIn: setIsLoggedIn, role: role, setRole: setRole, + userId: userId, + setUserId: setUserId, token: token, setToken: (value: string | null) => { // Log the user out if token is null diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index 01b921a18..efc015eea 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -1,10 +1,12 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { getProjects } from "../../utils/api/projects"; import "./ProjectsPage.css"; import { ProjectCard } from "../../components/ProjectsComponents"; -import { CardsGrid, CreateButton, SearchButton, SearchField } from "./styles"; +import { CardsGrid, CreateButton, SearchButton, SearchField, OwnProject } from "./styles"; + +import { useAuth } from "../../contexts/auth-context"; interface Partner { name: string; @@ -12,6 +14,7 @@ interface Partner { interface Coach { name: string; + userId: number; } interface Project { @@ -24,14 +27,47 @@ interface Project { } function ProjectPage() { + const [projectsAPI, setProjectsAPI] = useState>([]); const [projects, setProjects] = useState>([]); const [gotProjects, setGotProjects] = useState(false); + const [searchString, setSearchString] = useState(""); + const [ownProjects, setOwnProjects] = useState(false); + + const { userId } = useAuth(); + + const searchProjects = useCallback(() => { + const results: Project[] = []; + projectsAPI.forEach(project => { + let ownsProject = true; + if (ownProjects) { + ownsProject = false; + project.coaches.forEach(coach => { + if (coach.userId === userId) { + ownsProject = true; + } + }); + } + if ( + project.name.toLocaleLowerCase().includes(searchString.toLocaleLowerCase()) && + ownsProject + ) { + results.push(project); + } + }); + setProjects(results); + }, [ownProjects, projectsAPI, searchString, userId]); + + useEffect(() => { + searchProjects(); + }, [ownProjects, searchProjects]); + useEffect(() => { async function callProjects() { setGotProjects(true); const response = await getProjects("summerof2022"); if (response) { + setProjectsAPI(response.projects); setProjects(response.projects); } else setGotProjects(false); } @@ -43,10 +79,29 @@ function ProjectPage() { return (
                                        - - Search + setSearchString(e.target.value)} + onKeyPress={e => { + if (e.key === "Enter") { + searchProjects(); + } + }} + placeholder="project name" + > + Search Create Project
                                        + { + setOwnProjects(!ownProjects); + searchProjects(); + }} + /> {projects.map((project, _index) => ( diff --git a/frontend/src/views/ProjectsPage/styles.ts b/frontend/src/views/ProjectsPage/styles.ts index 109fa4bd9..80609aa7d 100644 --- a/frontend/src/views/ProjectsPage/styles.ts +++ b/frontend/src/views/ProjectsPage/styles.ts @@ -1,4 +1,5 @@ import styled from "styled-components"; +import { Form } from "react-bootstrap"; export const CardsGrid = styled.div` display: grid; @@ -32,3 +33,6 @@ export const CreateButton = styled.button` border: none; border-radius: 10px; ` +export const OwnProject = styled(Form.Check)` + margin-left: 20px; +` diff --git a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx index 82b2dbbdf..4cf771000 100644 --- a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx +++ b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx @@ -25,6 +25,7 @@ export default function VerifyingTokenPage() { setBearerToken(authContext.token); authContext.setIsLoggedIn(true); authContext.setRole(response.admin ? Role.ADMIN : Role.COACH); + authContext.setUserId(response.userId) } }; From 9ee62cd32da2cfd6b94fb808f3a78f0c0dfb4e90 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Wed, 6 Apr 2022 22:22:20 +0200 Subject: [PATCH 259/536] Seperate auth field --- backend/src/app/schemas/users.py | 25 ++++++++++--------- .../test_routers/test_users/test_users.py | 10 +++++--- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/backend/src/app/schemas/users.py b/backend/src/app/schemas/users.py index a301134b8..cbd55455c 100644 --- a/backend/src/app/schemas/users.py +++ b/backend/src/app/schemas/users.py @@ -2,14 +2,19 @@ from src.database.models import User as ModelUser +class Authentication(CamelCaseModel): + """Model for an authentication method""" + auth_type: str + email: str + + class User(CamelCaseModel): """Model for a user""" user_id: int name: str admin: bool - auth_type: str | None - email: str | None + auth: Authentication | None class Config: """Set to ORM mode""" @@ -18,24 +23,20 @@ class Config: def user_model_to_schema(model_user: ModelUser) -> User: """Create User Schema from User Model""" - auth_type: str | None = None - email: str | None = None + auth: Authentication | None = None if model_user.email_auth is not None: - auth_type = "email" - email = model_user.email_auth.email + auth = Authentication(auth_type="email", email=model_user.email_auth.email) elif model_user.github_auth is not None: - auth_type = "github" - email = model_user.github_auth.email + auth = Authentication(auth_type="github", email=model_user.github_auth.email) elif model_user.google_auth is not None: - auth_type = "google" - email = model_user.google_auth.email + auth = Authentication(auth_type="google", email=model_user.google_auth.email) return User( user_id=model_user.user_id, name=model_user.name, admin=model_user.admin, - auth_type=auth_type, - email=email) + auth=auth + ) class UsersListResponse(CamelCaseModel): diff --git a/backend/tests/test_routers/test_users/test_users.py b/backend/tests/test_routers/test_users/test_users.py index a0962ec66..04c1997d0 100644 --- a/backend/tests/test_routers/test_users/test_users.py +++ b/backend/tests/test_routers/test_users/test_users.py @@ -46,7 +46,9 @@ def data(database_session: Session) -> dict[str, str | int]: "edition1": edition1.name, "edition2": edition2.name, "email1": email_auth1.email, - "email2": github_auth1.email + "email2": github_auth1.email, + "auth_type1": "email", + "auth_type2": "github" } @@ -69,9 +71,11 @@ def test_get_users_response(database_session: Session, auth_client: AuthClient, response = auth_client.get("/users") users = response.json()["users"] user1 = [user for user in users if user["userId"] == data["user1"]][0] - assert user1["email"] == data["email1"] + 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["email"] == data["email2"] + 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]): From 2036606bc8bd26683910c8ccc1c4b29aad5c9b5a Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 6 Apr 2022 22:41:33 +0200 Subject: [PATCH 260/536] some imports updates --- frontend/src/data/interfaces/index.ts | 1 + frontend/src/data/interfaces/projects.ts | 17 +++++++ .../src/views/ProjectsPage/ProjectsPage.tsx | 48 +++++-------------- frontend/src/views/ProjectsPage/styles.ts | 5 +- 4 files changed, 33 insertions(+), 38 deletions(-) create mode 100644 frontend/src/data/interfaces/projects.ts diff --git a/frontend/src/data/interfaces/index.ts b/frontend/src/data/interfaces/index.ts index 6d7c3694d..b2ffe9df2 100644 --- a/frontend/src/data/interfaces/index.ts +++ b/frontend/src/data/interfaces/index.ts @@ -1 +1,2 @@ export type { User } from "./users"; +export type { Partner, Coach, Project} from "./projects" diff --git a/frontend/src/data/interfaces/projects.ts b/frontend/src/data/interfaces/projects.ts new file mode 100644 index 000000000..869161649 --- /dev/null +++ b/frontend/src/data/interfaces/projects.ts @@ -0,0 +1,17 @@ +export interface Partner { + name: string; +} + +export interface Coach { + name: string; + userId: number; +} + +export interface Project { + name: string; + numberOfStudents: number; + partners: Partner[]; + coaches: Coach[]; + editionName: string; + projectId: string; +} \ No newline at end of file diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/ProjectsPage/ProjectsPage.tsx index efc015eea..d49345785 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/ProjectsPage/ProjectsPage.tsx @@ -1,30 +1,10 @@ -import React, { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { getProjects } from "../../utils/api/projects"; import "./ProjectsPage.css"; - import { ProjectCard } from "../../components/ProjectsComponents"; - import { CardsGrid, CreateButton, SearchButton, SearchField, OwnProject } from "./styles"; - import { useAuth } from "../../contexts/auth-context"; - -interface Partner { - name: string; -} - -interface Coach { - name: string; - userId: number; -} - -interface Project { - name: string; - numberOfStudents: number; - partners: Partner[]; - coaches: Coach[]; - editionName: string; - projectId: string; -} +import { Project } from "../../data/interfaces"; function ProjectPage() { const [projectsAPI, setProjectsAPI] = useState>([]); @@ -36,7 +16,10 @@ function ProjectPage() { const { userId } = useAuth(); - const searchProjects = useCallback(() => { + /** + * Uses to filter the results based onto search string and own projects + */ + useEffect(() => { const results: Project[] = []; projectsAPI.forEach(project => { let ownsProject = true; @@ -56,12 +39,11 @@ function ProjectPage() { } }); setProjects(results); - }, [ownProjects, projectsAPI, searchString, userId]); - - useEffect(() => { - searchProjects(); - }, [ownProjects, searchProjects]); + }, [projectsAPI, ownProjects, searchString, userId]); + /** + * Used to fetch the projects + */ useEffect(() => { async function callProjects() { setGotProjects(true); @@ -82,14 +64,9 @@ function ProjectPage() { setSearchString(e.target.value)} - onKeyPress={e => { - if (e.key === "Enter") { - searchProjects(); - } - }} placeholder="project name" - > - Search + /> + Search Create Project
                                        { setOwnProjects(!ownProjects); - searchProjects(); }} /> diff --git a/frontend/src/views/ProjectsPage/styles.ts b/frontend/src/views/ProjectsPage/styles.ts index 80609aa7d..c39d7338f 100644 --- a/frontend/src/views/ProjectsPage/styles.ts +++ b/frontend/src/views/ProjectsPage/styles.ts @@ -9,7 +9,7 @@ export const CardsGrid = styled.div` `; export const SearchField = styled.input` - margin: 20px 5px 20px 20px; + margin: 20px 5px 5px 20px; padding: 5px 10px; background-color: #131329; color: white; @@ -26,7 +26,7 @@ export const SearchButton = styled.button` ` export const CreateButton = styled.button` - margin: 20px; + margin-left: 25px; padding: 5px 10px; background-color: #44dba4; color: white; @@ -34,5 +34,6 @@ export const CreateButton = styled.button` border-radius: 10px; ` export const OwnProject = styled(Form.Check)` + margin-top: 10px; margin-left: 20px; ` From e9a7fd84235fbc8740d423ff4952f196fd3d3a17 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Thu, 7 Apr 2022 15:23:22 +0200 Subject: [PATCH 261/536] Fix typing --- backend/src/app/routers/login/login.py | 2 +- backend/src/database/crud/users.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/src/app/routers/login/login.py b/backend/src/app/routers/login/login.py index d89cb03db..5f4b7afc0 100644 --- a/backend/src/app/routers/login/login.py +++ b/backend/src/app/routers/login/login.py @@ -19,7 +19,7 @@ @login_router.post("/token", response_model=Token) async def login_for_access_token(db: Session = Depends(get_session), - form_data: OAuth2PasswordRequestForm = Depends()) -> dict[str, str]: + 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) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index bc8ffa66a..400d897e2 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -20,7 +20,16 @@ def get_all_users(db: Session) -> list[User]: def get_user_edition_names(user: User) -> list[str]: """Get all names of the editions this user is coach in""" - return list(map(lambda e: e.name, user.editions)) + # Name is 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 + + editions = [] + for edition in user.editions: + if edition.name is not None: + editions.append(edition.name) + + return editions def get_users_from_edition(db: Session, edition_name: str) -> list[User]: From 5e3f032634bd5efed77e94b5ea179e0a65168083 Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 10:43:52 +0200 Subject: [PATCH 262/536] add pagination util --- backend/settings.py | 2 ++ backend/src/database/crud/util.py | 7 +++++++ 2 files changed, 9 insertions(+) create mode 100644 backend/src/database/crud/util.py diff --git a/backend/settings.py b/backend/settings.py index 5390e3e1b..9daa34dfa 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -26,6 +26,8 @@ DB_PORT: int = env.int("DB_PORT", 3306) # Option to change te database used. Default False is Mariadb. DB_USE_SQLITE: bool = env.bool("DB_USE_SQLITE", False) +# Option to change the pagination size for all endpoints that have pagination. +DB_PAGE_SIZE: int = env.int("DB_PAGE_SIZE", 100) """JWT token key""" SECRET_KEY: str = env.str("SECRET_KEY", "4d16a9cc83d74144322e893c879b5f639088c15dc1606b11226abbd7e97f5ee5") diff --git a/backend/src/database/crud/util.py b/backend/src/database/crud/util.py new file mode 100644 index 000000000..7ed40af75 --- /dev/null +++ b/backend/src/database/crud/util.py @@ -0,0 +1,7 @@ +from sqlalchemy.orm import Query + +import settings + + +def paginate(query: Query, page: int) -> Query: + return query.slice(page * settings.DB_PAGE_SIZE, (page + 1) * settings.DB_PAGE_SIZE) From 92b18d60457e30571689a6f3a0fe2105d1a7c785 Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 10:48:32 +0200 Subject: [PATCH 263/536] paginate editions --- backend/src/app/logic/editions.py | 12 +++--------- backend/src/app/routers/editions/editions.py | 7 ++++--- backend/src/database/crud/editions.py | 17 ++++++++++++++--- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/backend/src/app/logic/editions.py b/backend/src/app/logic/editions.py index 01bbcaea4..4aece6d75 100644 --- a/backend/src/app/logic/editions.py +++ b/backend/src/app/logic/editions.py @@ -4,16 +4,10 @@ from src.app.schemas.editions import EditionBase, EditionList from src.database.models import Edition as EditionModel -def get_editions(db: Session) -> EditionList: - """Get a list of all editions. - Args: - db (Session): connection with the database. - - Returns: - EditionList: an object with a list of all the editions. - """ - return EditionList(editions=crud_editions.get_editions(db)) +def get_editions(db: Session, page: int) -> EditionList: + """Get a paginated list of all editions.""" + return EditionList(editions=crud_editions.get_editions_page(db, page)) def get_edition_by_name(db: Session, edition_name: str) -> EditionModel: diff --git a/backend/src/app/routers/editions/editions.py b/backend/src/app/routers/editions/editions.py index 5c569b948..bf6c41684 100644 --- a/backend/src/app/routers/editions/editions.py +++ b/backend/src/app/routers/editions/editions.py @@ -34,16 +34,17 @@ @editions_router.get("/", response_model=EditionList, tags=[Tags.EDITIONS], dependencies=[Depends(require_auth)]) -async def get_editions(db: Session = Depends(get_session)): - """Get a list of all editions. +async def get_editions(db: Session = Depends(get_session), page: int = 0): + """Get a paginated list of all editions. Args: db (Session, optional): connection with the database. Defaults to Depends(get_session). + page (int): the page to return. Returns: EditionList: an object with a list of all the editions. """ # TODO only return editions the user can see - return logic_editions.get_editions(db) + return logic_editions.get_editions(db, page) @editions_router.get("/{edition_name}", response_model=Edition, tags=[Tags.EDITIONS], diff --git a/backend/src/database/crud/editions.py b/backend/src/database/crud/editions.py index 6532c185b..37e279320 100644 --- a/backend/src/database/crud/editions.py +++ b/backend/src/database/crud/editions.py @@ -1,8 +1,11 @@ -from sqlalchemy.orm import Session from sqlalchemy import exc +from sqlalchemy.orm import Query +from sqlalchemy.orm import Session + from src.app.exceptions.editions import DuplicateInsertException -from src.database.models import Edition 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: @@ -19,6 +22,10 @@ def get_edition_by_name(db: Session, edition_name: str) -> Edition: return db.query(Edition).where(Edition.name == edition_name).one() +def _get_editions_query(db: Session) -> Query: + return db.query(Edition) + + def get_editions(db: Session) -> list[Edition]: """Get a list of all editions. @@ -28,7 +35,11 @@ def get_editions(db: Session) -> list[Edition]: Returns: EditionList: an object with a list of all editions """ - return db.query(Edition).all() + return _get_editions_query(db).all() + + +def get_editions_page(db: Session, page: int) -> list[Edition]: + return paginate(_get_editions_query(db), page).all() def create_edition(db: Session, edition: EditionBase) -> Edition: From 43b403f84303b7b2205af40132cb6d45fa1913d3 Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 11:25:38 +0200 Subject: [PATCH 264/536] change function name --- backend/src/app/logic/editions.py | 2 +- backend/src/app/routers/editions/editions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/app/logic/editions.py b/backend/src/app/logic/editions.py index 4aece6d75..da3eb4e5a 100644 --- a/backend/src/app/logic/editions.py +++ b/backend/src/app/logic/editions.py @@ -5,7 +5,7 @@ from src.database.models import Edition as EditionModel -def get_editions(db: Session, page: int) -> EditionList: +def get_editions_page(db: Session, page: int) -> EditionList: """Get a paginated list of all editions.""" return EditionList(editions=crud_editions.get_editions_page(db, page)) diff --git a/backend/src/app/routers/editions/editions.py b/backend/src/app/routers/editions/editions.py index bf6c41684..eae00dddf 100644 --- a/backend/src/app/routers/editions/editions.py +++ b/backend/src/app/routers/editions/editions.py @@ -44,7 +44,7 @@ async def get_editions(db: Session = Depends(get_session), page: int = 0): EditionList: an object with a list of all the editions. """ # TODO only return editions the user can see - return logic_editions.get_editions(db, page) + return logic_editions.get_editions_page(db, page) @editions_router.get("/{edition_name}", response_model=Edition, tags=[Tags.EDITIONS], From caa30e2cd20f34305cbac46adcdbc721c5e39c8e Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 11:26:11 +0200 Subject: [PATCH 265/536] paginate invites --- backend/src/app/logic/invites.py | 14 ++++++------ .../app/routers/editions/invites/invites.py | 10 ++++----- backend/src/app/schemas/invites.py | 2 +- backend/src/database/crud/invites.py | 15 ++++++++++--- .../test_database/test_crud/test_invites.py | 22 +++++++++---------- .../test_invites/test_invites.py | 1 + 6 files changed, 37 insertions(+), 27 deletions(-) diff --git a/backend/src/app/logic/invites.py b/backend/src/app/logic/invites.py index b5b98abdc..1e18c03aa 100644 --- a/backend/src/app/logic/invites.py +++ b/backend/src/app/logic/invites.py @@ -1,8 +1,8 @@ from sqlalchemy.orm import Session -from src.app.schemas.invites import InvitesListResponse, EmailAddress, NewInviteLink, InviteLink as InviteLinkModel +from src.app.schemas.invites import InvitesLinkList, EmailAddress, NewInviteLink, InviteLink as InviteLinkModel from src.app.utils.mailto import generate_mailto_string -from src.database.crud.invites import create_invite_link, delete_invite_link as delete_link_db, get_all_pending_invites +import src.database.crud.invites as crud from src.database.models import Edition, InviteLink as InviteLinkDB import settings @@ -10,27 +10,27 @@ def delete_invite_link(db: Session, invite_link: InviteLinkDB): """Delete an invite link from the database""" - delete_link_db(db, invite_link) + crud.delete_invite_link(db, invite_link) -def get_pending_invites_list(db: Session, edition: Edition) -> InvitesListResponse: +def get_pending_invites_page(db: Session, edition: Edition, page: int) -> InvitesLinkList: """ Query the database for a list of invite links and wrap the result in a pydantic model """ - invites_orm = get_all_pending_invites(db, edition) + invites_orm = crud.get_pending_invites_for_edition_page(db, edition, page) invites = [] for invite in invites_orm: new_invite = InviteLinkModel(invite_link_id=invite.invite_link_id, uuid=invite.uuid, target_email=invite.target_email, edition_name=edition.name) invites.append(new_invite) - return InvitesListResponse(invite_links=invites) + return InvitesLinkList(invite_links=invites) def create_mailto_link(db: Session, edition: Edition, email_address: EmailAddress) -> NewInviteLink: """Add a new invite link into the database & return a mailto link for it""" # Create db entry - new_link_db = create_invite_link(db, edition, email_address.email) + new_link_db = crud.create_invite_link(db, edition, email_address.email) # Create endpoint for the user to click on link = f"{settings.FRONTEND_URL}/register/{new_link_db.uuid}" diff --git a/backend/src/app/routers/editions/invites/invites.py b/backend/src/app/routers/editions/invites/invites.py index e500491c9..813bf7d9e 100644 --- a/backend/src/app/routers/editions/invites/invites.py +++ b/backend/src/app/routers/editions/invites/invites.py @@ -3,9 +3,9 @@ from starlette import status from starlette.responses import Response -from src.app.logic.invites import create_mailto_link, delete_invite_link, get_pending_invites_list +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.schemas.invites import InvitesListResponse, EmailAddress, NewInviteLink, InviteLink as InviteLinkModel +from src.app.schemas.invites import InvitesLinkList, EmailAddress, NewInviteLink, InviteLink as InviteLinkModel from src.app.utils.dependencies import get_edition, get_invite_link, require_admin from src.database.database import get_session from src.database.models import Edition, InviteLink as InviteLinkDB @@ -13,12 +13,12 @@ invites_router = APIRouter(prefix="/invites", tags=[Tags.INVITES]) -@invites_router.get("/", response_model=InvitesListResponse, dependencies=[Depends(require_admin)]) -async def get_invites(db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): +@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): """ Get a list of all pending invitation links. """ - return get_pending_invites_list(db, edition) + return get_pending_invites_page(db, edition, page) @invites_router.post("/", status_code=status.HTTP_201_CREATED, response_model=NewInviteLink, diff --git a/backend/src/app/schemas/invites.py b/backend/src/app/schemas/invites.py index cbd6327f5..cdea6258f 100644 --- a/backend/src/app/schemas/invites.py +++ b/backend/src/app/schemas/invites.py @@ -33,7 +33,7 @@ class Config: orm_mode = True -class InvitesListResponse(CamelCaseModel): +class InvitesLinkList(CamelCaseModel): """A list of invite link models Sending a pure list as JSON is bad practice, lists should be wrapped in a dict with 1 key that leads to them instead. This class handles that. diff --git a/backend/src/database/crud/invites.py b/backend/src/database/crud/invites.py index 078b4c87e..4ba7c9d7b 100644 --- a/backend/src/database/crud/invites.py +++ b/backend/src/database/crud/invites.py @@ -1,8 +1,9 @@ from uuid import UUID -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, Query from src.app.exceptions.parsing import MalformedUUIDError +from src.database.crud.util import paginate from src.database.models import Edition, InviteLink @@ -22,9 +23,17 @@ def delete_invite_link(db: Session, invite_link: InviteLink, commit: bool = True db.commit() -def get_all_pending_invites(db: Session, edition: Edition) -> list[InviteLink]: +def _get_pending_invites_for_edition_query(db: Session, edition: Edition) -> Query: """Return a list of all invite links in a given edition""" - return db.query(InviteLink).where(InviteLink.edition == edition).all() + return db.query(InviteLink).where(InviteLink.edition == edition).order_by(InviteLink.invite_link_id) + + +def get_pending_invites_for_edition(db: Session, edition: Edition) -> list[InviteLink]: + return _get_pending_invites_for_edition_query(db, edition).all() + + +def get_pending_invites_for_edition_page(db: Session, edition: Edition, page: int) -> list[InviteLink]: + return paginate(_get_pending_invites_for_edition_query(db, edition), page).all() def get_invite_link_by_uuid(db: Session, invite_uuid: str | UUID) -> InviteLink: diff --git a/backend/tests/test_database/test_crud/test_invites.py b/backend/tests/test_database/test_crud/test_invites.py index 8643ddde7..18e54108d 100644 --- a/backend/tests/test_database/test_crud/test_invites.py +++ b/backend/tests/test_database/test_crud/test_invites.py @@ -5,7 +5,7 @@ from sqlalchemy.orm import Session from src.app.exceptions.parsing import MalformedUUIDError -from src.database.crud.invites import create_invite_link, delete_invite_link, get_all_pending_invites, \ +from src.database.crud.invites import create_invite_link, delete_invite_link, get_pending_invites_for_edition, \ get_invite_link_by_uuid from src.database.models import Edition, InviteLink @@ -17,12 +17,12 @@ def test_create_invite_link(database_session: Session): database_session.commit() # Db empty - assert len(get_all_pending_invites(database_session, edition)) == 0 + assert len(get_pending_invites_for_edition(database_session, edition)) == 0 # Create new link create_invite_link(database_session, edition, "test@ema.il") - assert len(get_all_pending_invites(database_session, edition)) == 1 + assert len(get_pending_invites_for_edition(database_session, edition)) == 1 def test_delete_invite_link(database_session: Session): @@ -34,9 +34,9 @@ def test_delete_invite_link(database_session: Session): # Create new link new_link = create_invite_link(database_session, edition, "test@ema.il") - assert len(get_all_pending_invites(database_session, edition)) == 1 + assert len(get_pending_invites_for_edition(database_session, edition)) == 1 delete_invite_link(database_session, new_link) - assert len(get_all_pending_invites(database_session, edition)) == 0 + assert len(get_pending_invites_for_edition(database_session, edition)) == 0 def test_get_all_pending_invites_empty(database_session: Session): @@ -48,8 +48,8 @@ def test_get_all_pending_invites_empty(database_session: Session): database_session.commit() # Db empty - assert len(get_all_pending_invites(database_session, edition_one)) == 0 - assert len(get_all_pending_invites(database_session, edition_two)) == 0 + assert len(get_pending_invites_for_edition(database_session, edition_one)) == 0 + assert len(get_pending_invites_for_edition(database_session, edition_two)) == 0 def test_get_all_pending_invites_one_present(database_session: Session): @@ -68,10 +68,10 @@ def test_get_all_pending_invites_one_present(database_session: Session): database_session.add(link_one) database_session.commit() - assert len(get_all_pending_invites(database_session, edition_one)) == 1 + assert len(get_pending_invites_for_edition(database_session, edition_one)) == 1 # Other edition still empty - assert len(get_all_pending_invites(database_session, edition_two)) == 0 + assert len(get_pending_invites_for_edition(database_session, edition_two)) == 0 def test_get_all_pending_invites_two_present(database_session: Session): @@ -89,8 +89,8 @@ def test_get_all_pending_invites_two_present(database_session: Session): database_session.add(link_two) database_session.commit() - assert len(get_all_pending_invites(database_session, edition_one)) == 1 - assert len(get_all_pending_invites(database_session, edition_two)) == 1 + assert len(get_pending_invites_for_edition(database_session, edition_one)) == 1 + assert len(get_pending_invites_for_edition(database_session, edition_two)) == 1 def test_get_invite_link_by_uuid_existing(database_session: Session): 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 4f06741e4..97f0af1e2 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 @@ -31,6 +31,7 @@ def test_get_invites(database_session: Session, auth_client: AuthClient): response = auth_client.get("/editions/ed2022/invites") + print(response.json()) assert response.status_code == status.HTTP_200_OK json = response.json() assert len(json["inviteLinks"]) == 1 From d83afcf945c26c9d093370a0102926fa9d86c862 Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 12:00:27 +0200 Subject: [PATCH 266/536] paginate projects --- backend/src/app/logic/projects.py | 13 +++++++------ .../app/routers/editions/projects/projects.py | 7 +++---- backend/src/database/crud/projects.py | 19 ++++++++++++++----- .../test_database/test_crud/test_projects.py | 18 +++++++++--------- 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index 6d51d102a..27c57526e 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -1,15 +1,16 @@ from sqlalchemy.orm import Session -from src.app.schemas.projects import ProjectList, Project, ConflictStudentList, InputProject, Student, \ - ConflictStudent, ConflictProject -from src.database.crud.projects import db_get_all_projects, db_add_project, db_delete_project, \ +from src.app.schemas.projects import ( + ProjectList, Project, ConflictStudentList, InputProject, Student, ConflictStudent, ConflictProject +) +from src.database.crud.projects import db_get_projects_for_edition_page, db_add_project, db_delete_project, \ db_patch_project, db_get_conflict_students -from src.database.models import Edition, Project as ProjectModel +from src.database.models import Edition -def logic_get_project_list(db: Session, edition: Edition) -> ProjectList: +def logic_get_project_list(db: Session, edition: Edition, page: int) -> ProjectList: """Returns a list of all projects from a certain edition""" - db_all_projects = db_get_all_projects(db, edition) + db_all_projects = db_get_projects_for_edition_page(db, edition, page) projects_model = [] for project in db_all_projects: project_model = Project(project_id=project.project_id, name=project.name, diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index e7b12712d..cb190cb34 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -6,8 +6,7 @@ from src.app.logic.projects import logic_get_project_list, logic_create_project, logic_delete_project, \ logic_patch_project, logic_get_conflicts from src.app.routers.tags import Tags -from src.app.schemas.projects import ProjectList, Project, InputProject, \ - ConflictStudentList +from src.app.schemas.projects import ProjectList, Project, InputProject, ConflictStudentList from src.app.utils.dependencies import get_edition, get_project, require_admin, require_coach from src.database.database import get_session from src.database.models import Edition, Project as ProjectModel @@ -18,11 +17,11 @@ @projects_router.get("/", response_model=ProjectList, dependencies=[Depends(require_coach)]) -async def get_projects(db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): +async def get_projects(db: Session = Depends(get_session), edition: Edition = Depends(get_edition), page: int = 0): """ Get a list of all projects. """ - return logic_get_project_list(db, edition) + return logic_get_project_list(db, edition, page) @projects_router.post("/", status_code=status.HTTP_201_CREATED, response_model=Project, diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index f9d4233b1..ee6fc650f 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -1,13 +1,22 @@ from sqlalchemy.exc import NoResultFound -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, Query -from src.app.schemas.projects import ConflictStudent, InputProject +from src.app.schemas.projects import InputProject +from src.database.crud.util import paginate from src.database.models import Project, Edition, Student, ProjectRole, Skill, User, Partner -def db_get_all_projects(db: Session, edition: Edition) -> list[Project]: +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 db_get_projects_for_edition(db: Session, edition: Edition) -> list[Project]: """Query all projects from a certain edition from the database""" - return db.query(Project).where(Project.edition == edition).all() + return _get_projects_for_edition_query(db, edition).all() + + +def db_get_projects_for_edition_page(db: Session, edition: Edition, page: int) -> list[Project]: + return paginate(_get_projects_for_edition_query(db, edition), page).all() def db_add_project(db: Session, edition: Edition, input_project: InputProject) -> Project: @@ -57,7 +66,7 @@ def db_patch_project(db: Session, project_id: int, input_project: InputProject): 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 = [] diff --git a/backend/tests/test_database/test_crud/test_projects.py b/backend/tests/test_database/test_crud/test_projects.py index cc5f6b6e2..2762dcbc0 100644 --- a/backend/tests/test_database/test_crud/test_projects.py +++ b/backend/tests/test_database/test_crud/test_projects.py @@ -3,7 +3,7 @@ from sqlalchemy.exc import NoResultFound from src.app.schemas.projects import ConflictStudent, InputProject -from src.database.crud.projects import (db_get_all_projects, db_add_project, +from src.database.crud.projects import (db_get_projects_for_edition, db_add_project, db_get_project, db_delete_project, db_patch_project, db_get_conflict_students) from src.database.models import Edition, Partner, Project, User, Skill, ProjectRole, Student @@ -59,14 +59,14 @@ def test_get_all_projects_empty(database_session: Session): edition: Edition = Edition(year=2022, name="ed2022") database_session.add(edition) database_session.commit() - projects: list[Project] = db_get_all_projects( + projects: list[Project] = db_get_projects_for_edition( database_session, edition) assert len(projects) == 0 def test_get_all_projects(database_with_data: Session, current_edition: Edition): """test get all projects""" - projects: list[Project] = db_get_all_projects( + projects: list[Project] = db_get_projects_for_edition( database_with_data, current_edition) assert len(projects) == 3 @@ -137,10 +137,10 @@ def test_delete_project_no_project_roles(database_with_data: Session, current_ed """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(db_get_all_projects(database_with_data, current_edition)) == 3 + assert len(db_get_projects_for_edition(database_with_data, current_edition)) == 3 db_delete_project(database_with_data, 3) - assert len(db_get_all_projects(database_with_data, current_edition)) == 2 - assert 3 not in [project.project_id for project in db_get_all_projects( + assert len(db_get_projects_for_edition(database_with_data, current_edition)) == 2 + assert 3 not in [project.project_id for project in db_get_projects_for_edition( database_with_data, current_edition)] @@ -148,10 +148,10 @@ def test_delete_project_with_project_roles(database_with_data: Session, current_ """test delete a project that has project roles""" assert len(database_with_data.query(ProjectRole).where( ProjectRole.project_id == 1).all()) > 0 - assert len(db_get_all_projects(database_with_data, current_edition)) == 3 + assert len(db_get_projects_for_edition(database_with_data, current_edition)) == 3 db_delete_project(database_with_data, 1) - assert len(db_get_all_projects(database_with_data, current_edition)) == 2 - assert 1 not in [project.project_id for project in db_get_all_projects( + assert len(db_get_projects_for_edition(database_with_data, current_edition)) == 2 + assert 1 not in [project.project_id for project in db_get_projects_for_edition( database_with_data, current_edition)] assert len(database_with_data.query(ProjectRole).where( ProjectRole.project_id == 1).all()) == 0 From 70a1f89c7ed78c07b231cfd3c5bf53cd3df4caf8 Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 12:37:15 +0200 Subject: [PATCH 267/536] paginate users --- backend/src/app/logic/users.py | 24 ++-- backend/src/app/routers/users/users.py | 8 +- backend/src/database/crud/users.py | 110 ++++++++++++------ .../test_database/test_crud/test_users.py | 18 +-- 4 files changed, 97 insertions(+), 63 deletions(-) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index 35dd8d888..69d4c0898 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -5,22 +5,21 @@ from src.database.models import User -def get_users_list(db: Session, admin: bool, edition_name: str | None) -> UsersListResponse: +def get_users_list(db: Session, admin: bool, edition_name: str | None, page: int) -> UsersListResponse: """ Query the database for a list of users and wrap the result in a pydantic model """ - if admin: if edition_name is None: - users_orm = users_crud.get_all_admins(db) + users_orm = users_crud.get_admins(db) else: - users_orm = users_crud.get_admins_from_edition(db, edition_name) + users_orm = users_crud.get_admins_for_edition(db, edition_name) else: if edition_name is None: - users_orm = users_crud.get_all_users(db) + users_orm = users_crud.get_users_page(db, page) else: - users_orm = users_crud.get_users_from_edition(db, edition_name) + users_orm = users_crud.get_users_for_edition_page(db, edition_name, page) users = [] for user in users_orm: @@ -37,7 +36,6 @@ def edit_admin_status(db: Session, user_id: int, admin: AdminPatch): """ Edit the admin-status of a user """ - users_crud.edit_admin_status(db, user_id, admin.admin) @@ -45,7 +43,6 @@ def add_coach(db: Session, user_id: int, edition_name: str): """ Add user as coach for the given edition """ - users_crud.add_coach(db, user_id, edition_name) @@ -53,7 +50,6 @@ def remove_coach(db: Session, user_id: int, edition_name: str): """ Remove user as coach for the given edition """ - users_crud.remove_coach(db, user_id, edition_name) @@ -61,20 +57,18 @@ def remove_coach_all_editions(db: Session, user_id: int): """ Remove user as coach from all editions """ - users_crud.remove_coach_all_editions(db, user_id) -def get_request_list(db: Session, edition_name: str | None) -> UserRequestsResponse: +def get_request_list(db: Session, edition_name: str | None, page: int) -> UserRequestsResponse: """ Query the database for a list of all user requests and wrap the result in a pydantic model """ - if edition_name is None: - requests = users_crud.get_all_requests(db) + requests = users_crud.get_requests_page(db, page) else: - requests = users_crud.get_all_requests_from_edition(db, edition_name) + requests = users_crud.get_requests_for_edition_page(db, edition_name, page) requests_model = [] for request in requests: @@ -87,7 +81,6 @@ def accept_request(db: Session, request_id: int): """ Accept user request """ - users_crud.accept_request(db, request_id) @@ -95,5 +88,4 @@ def reject_request(db: Session, request_id: int): """ Reject user request """ - users_crud.reject_request(db, request_id) diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index edcde7166..2b246cc0d 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -12,12 +12,12 @@ @users_router.get("/", response_model=UsersListResponse, dependencies=[Depends(require_admin)]) -async def get_users(admin: bool = Query(False), edition: str | None = Query(None), db: Session = Depends(get_session)): +async def get_users(admin: bool = Query(False), edition: str | None = Query(None), page: int = 0, db: Session = Depends(get_session)): """ Get users """ - return logic.get_users_list(db, admin, edition) + return logic.get_users_list(db, admin, edition, page) @users_router.get("/current", response_model=UserSchema) @@ -63,12 +63,12 @@ async def remove_from_all_editions(user_id: int, db: Session = Depends(get_sessi @users_router.get("/requests", response_model=UserRequestsResponse, dependencies=[Depends(require_admin)]) -async def get_requests(edition: str | None = Query(None), db: Session = Depends(get_session)): +async def get_requests(edition: str | None = Query(None), page: int = 0, db: Session = Depends(get_session)): """ Get pending userrequests """ - return logic.get_request_list(db, edition) + return logic.get_request_list(db, edition, page) @users_router.post("/requests/{request_id}/accept", status_code=204, dependencies=[Depends(require_admin)]) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index ceb869bcd..a9a4aeea2 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -1,26 +1,36 @@ -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, Query + +from src.database.crud.editions import get_edition_by_name +from src.database.crud.util import paginate from src.database.models import user_editions, User, Edition, CoachRequest, AuthGoogle, AuthEmail, AuthGitHub -def get_all_admins(db: Session) -> list[User]: +def get_admins(db: Session) -> list[User]: """ Get all admins """ - return db.query(User)\ - .where(User.admin)\ - .join(AuthEmail, isouter=True)\ - .join(AuthGitHub, isouter=True)\ - .join(AuthGoogle, isouter=True)\ + return db.query(User) \ + .where(User.admin) \ + .join(AuthEmail, isouter=True) \ + .join(AuthGitHub, isouter=True) \ + .join(AuthGoogle, isouter=True) \ .all() -def get_all_users(db: Session) -> list[User]: +def _get_users_query(db: Session) -> Query: + return db.query(User) + + +def get_users(db: Session) -> list[User]: """ Get all users (coaches + admins) """ + return _get_users_query(db).all() - return db.query(User).all() + +def get_users_page(db: Session, page: int) -> list[User]: + return paginate(_get_users_query(db), page).all() def get_user_edition_names(user: User) -> list[str]: @@ -28,7 +38,6 @@ def get_user_edition_names(user: User) -> list[str]: # Name is 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 - editions = [] for edition in user.editions: if edition.name is not None: @@ -37,31 +46,49 @@ def get_user_edition_names(user: User) -> list[str]: return editions -def get_users_from_edition(db: Session, edition_name: str) -> list[User]: +def _get_users_for_edition_query(db: Session, edition: Edition) -> Query: + return db.query(User).join(user_editions).filter(user_editions.c.edition_id == edition.edition_id) + + +def get_users_for_edition(db: Session, edition_name: str) -> list[User]: """ Get all coaches from the given edition """ - edition = db.query(Edition).where(Edition.name == edition_name).one() - return db.query(User).join(user_editions).filter(user_editions.c.edition_id == edition.edition_id).all() + return _get_users_for_edition_query(db, get_edition_by_name(db, edition_name)).all() -def get_admins_from_edition(db: Session, edition_name: str) -> list[User]: +def get_users_for_edition_page(db: Session, edition_name: str, page: int) -> list[User]: + """ + Get all coaches from the given edition + """ + return paginate(_get_users_for_edition_query(db, get_edition_by_name(db, edition_name)), page).all() + + +def _get_admins_for_edition_query(db: Session, edition: Edition) -> Query: + return db.query(User) \ + .where(User.admin) \ + .join(user_editions) \ + .filter(user_editions.c.edition_id == edition.edition_id) + + +def get_admins_for_edition(db: Session, edition_name: str) -> list[User]: """ Get all admins from the given edition """ - edition = db.query(Edition).where(Edition.name == edition_name).one() - return db.query(User)\ - .where(User.admin)\ - .join(user_editions)\ - .filter(user_editions.c.edition_id == edition.edition_id)\ - .all() + return _get_admins_for_edition_query(db, get_edition_by_name(db, edition_name)).all() + + +def get_admins_for_edition_page(db: Session, edition_name: str, page: int) -> list[User]: + """ + Get all admins from the given edition + """ + return paginate(_get_admins_for_edition_query(db, get_edition_by_name(db, edition_name)), page).all() def edit_admin_status(db: Session, user_id: int, admin: bool): """ Edit the admin-status of a user """ - user = db.query(User).where(User.user_id == user_id).one() user.admin = admin db.add(user) @@ -72,7 +99,6 @@ def add_coach(db: Session, 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.editions.append(edition) @@ -83,11 +109,10 @@ def remove_coach(db: Session, 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)\ - .where(user_editions.c.user_id == user_id)\ - .where(user_editions.c.edition_id == edition.edition_id)\ + db.query(user_editions) \ + .where(user_editions.c.user_id == user_id) \ + .where(user_editions.c.edition_id == edition.edition_id) \ .delete() db.commit() @@ -96,32 +121,50 @@ def remove_coach_all_editions(db: Session, 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() -def get_all_requests(db: Session) -> list[CoachRequest]: +def _get_requests_query(db: Session) -> Query: + return db.query(CoachRequest).join(User) + + +def get_requests(db: Session) -> list[CoachRequest]: """ Get all userrequests """ + return _get_requests_query(db).all() + - return db.query(CoachRequest).join(User).all() +def get_requests_page(db: Session, page: int) -> list[CoachRequest]: + """ + Get all userrequests + """ + return paginate(_get_requests_query(db), page).all() -def get_all_requests_from_edition(db: Session, edition_name: str) -> list[CoachRequest]: +def _get_requests_for_edition_query(db: Session, edition: Edition) -> Query: + return db.query(CoachRequest).where(CoachRequest.edition_id == edition.edition_id).join(User) + + +def get_requests_for_edition(db: Session, edition_name: str) -> list[CoachRequest]: """ Get all userrequests from a given edition """ - edition = db.query(Edition).where(Edition.name == edition_name).one() - return db.query(CoachRequest).where(CoachRequest.edition_id == edition.edition_id).join(User).all() + return _get_requests_for_edition_query(db, get_edition_by_name(db, edition_name)).all() + + +def get_requests_for_edition_page(db: Session, edition_name: str, page: int) -> list[CoachRequest]: + """ + Get all userrequests from a given edition + """ + return paginate(_get_requests_for_edition_query(db, get_edition_by_name(db, edition_name)), page).all() def accept_request(db: Session, 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) @@ -132,5 +175,4 @@ def reject_request(db: Session, request_id: int): """ Remove request """ - db.query(CoachRequest).where(CoachRequest.request_id == request_id).delete() diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index 660af7479..4f789f793 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -49,7 +49,7 @@ def test_get_all_users(database_session: Session, data: dict[str, int]): """Test get request for users""" # get all users - users = users_crud.get_all_users(database_session) + users = users_crud.get_users(database_session) assert len(users) == 2, "Wrong length" user_ids = [user.user_id for user in users] assert data["user1"] in user_ids @@ -60,7 +60,7 @@ def test_get_all_admins(database_session: Session, data: dict[str, str]): """Test get request for admins""" # get all admins - users = users_crud.get_all_admins(database_session) + users = users_crud.get_admins(database_session) assert len(users) == 1, "Wrong length" assert data["user1"] == users[0].user_id @@ -102,13 +102,13 @@ def test_get_all_users_from_edition(database_session: Session, data: dict[str, s """Test get request for users of a given edition""" # get all users from edition - users = users_crud.get_users_from_edition(database_session, data["edition1"]) + users = users_crud.get_users_for_edition(database_session, 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_from_edition(database_session, data["edition2"]) + users = users_crud.get_users_for_edition(database_session, data["edition2"]) assert len(users) == 1, "Wrong length" assert data["user2"] == users[0].user_id @@ -117,11 +117,11 @@ def test_get_admins_from_edition(database_session: Session, data: dict[str, str] """Test get request for admins of a given edition""" # get all admins from edition - users = users_crud.get_admins_from_edition(database_session, data["edition1"]) + users = users_crud.get_admins_for_edition(database_session, data["edition1"]) assert len(users) == 1, "Wrong length" assert data["user1"] == users[0].user_id - users = users_crud.get_admins_from_edition(database_session, data["edition2"]) + users = users_crud.get_admins_for_edition(database_session, data["edition2"]) assert len(users) == 0, "Wrong length" @@ -239,7 +239,7 @@ def test_get_all_requests(database_session: Session): database_session.commit() - requests = users_crud.get_all_requests(database_session) + requests = users_crud.get_requests(database_session) assert len(requests) == 2 assert request1 in requests assert request2 in requests @@ -273,11 +273,11 @@ def test_get_all_requests_from_edition(database_session: Session): database_session.commit() - requests = users_crud.get_all_requests_from_edition(database_session, edition1.name) + requests = users_crud.get_requests_for_edition(database_session, edition1.name) assert len(requests) == 1 assert requests[0].user == user1 - requests = users_crud.get_all_requests_from_edition(database_session, edition2.name) + requests = users_crud.get_requests_for_edition(database_session, edition2.name) assert len(requests) == 1 assert requests[0].user == user2 From 797627931af808cc4a98dbc60bd282c8c5bea276 Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 12:47:41 +0200 Subject: [PATCH 268/536] formating --- backend/src/app/app.py | 3 +-- backend/src/app/logic/invites.py | 5 ++--- backend/src/app/logic/register.py | 8 ++++---- backend/src/app/logic/skills.py | 2 +- backend/src/app/logic/users.py | 2 +- backend/src/app/routers/editions/editions.py | 11 ++++------- .../src/app/routers/editions/students/students.py | 1 - backend/src/app/routers/login/login.py | 2 +- backend/src/app/routers/skills/skills.py | 2 +- backend/src/app/routers/users/users.py | 13 +++---------- backend/src/app/schemas/invites.py | 2 +- backend/src/app/schemas/projects.py | 1 - backend/src/app/schemas/register.py | 1 + backend/src/app/utils/dependencies.py | 2 +- backend/src/database/crud/projects.py | 2 +- backend/src/database/crud/projects_students.py | 4 ++-- backend/src/database/crud/skills.py | 3 ++- 17 files changed, 26 insertions(+), 38 deletions(-) diff --git a/backend/src/app/app.py b/backend/src/app/app.py index 08ec01c22..520777d06 100644 --- a/backend/src/app/app.py +++ b/backend/src/app/app.py @@ -7,11 +7,10 @@ import settings from src.database.engine import engine from src.database.exceptions import PendingMigrationsException -from .routers import editions_router, login_router, skills_router from .exceptions import install_handlers +from .routers import editions_router, login_router, skills_router from .routers.users.users import users_router - # Main application app = FastAPI( title="OSOC Team 3", diff --git a/backend/src/app/logic/invites.py b/backend/src/app/logic/invites.py index 1e18c03aa..85ed867d7 100644 --- a/backend/src/app/logic/invites.py +++ b/backend/src/app/logic/invites.py @@ -1,12 +1,11 @@ from sqlalchemy.orm import Session +import settings +import src.database.crud.invites as crud from src.app.schemas.invites import InvitesLinkList, EmailAddress, NewInviteLink, InviteLink as InviteLinkModel from src.app.utils.mailto import generate_mailto_string -import src.database.crud.invites as crud from src.database.models import Edition, InviteLink as InviteLinkDB -import settings - def delete_invite_link(db: Session, invite_link: InviteLinkDB): """Delete an invite link from the database""" diff --git a/backend/src/app/logic/register.py b/backend/src/app/logic/register.py index 9a1a5c3bb..6e79af1db 100644 --- a/backend/src/app/logic/register.py +++ b/backend/src/app/logic/register.py @@ -1,12 +1,12 @@ import sqlalchemy.exc from sqlalchemy.orm import Session -from src.app.schemas.register import NewUser -from src.database.models import Edition, InviteLink -from src.database.crud.register import create_coach_request, create_user, create_auth_email -from src.database.crud.invites import get_invite_link_by_uuid, delete_invite_link 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.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.models import Edition, InviteLink def create_request(db: Session, new_user: NewUser, edition: Edition) -> None: diff --git a/backend/src/app/logic/skills.py b/backend/src/app/logic/skills.py index 00825ba52..19a86d308 100644 --- a/backend/src/app/logic/skills.py +++ b/backend/src/app/logic/skills.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import Session -from src.app.schemas.skills import SkillBase, SkillList import src.database.crud.skills as crud_skills +from src.app.schemas.skills import SkillBase, SkillList from src.database.models import Skill diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index 69d4c0898..93aaaf499 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import Session -from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, UserRequest, user_model_to_schema import src.database.crud.users as users_crud +from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, UserRequest, user_model_to_schema from src.database.models import User diff --git a/backend/src/app/routers/editions/editions.py b/backend/src/app/routers/editions/editions.py index eae00dddf..08928f12e 100644 --- a/backend/src/app/routers/editions/editions.py +++ b/backend/src/app/routers/editions/editions.py @@ -1,19 +1,16 @@ from fastapi import APIRouter, Depends -from starlette import status from sqlalchemy.orm import Session +from starlette import status +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.database.database import get_session -from src.app.logic import editions as logic_editions - 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 - # Don't add the "Editions" tag here, because then it gets applied # to all child routes as well from ...utils.dependencies import require_admin, require_auth, require_coach @@ -47,7 +44,7 @@ async def get_editions(db: Session = Depends(get_session), page: int = 0): return logic_editions.get_editions_page(db, page) -@editions_router.get("/{edition_name}", response_model=Edition, tags=[Tags.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. @@ -76,7 +73,7 @@ async def post_edition(edition: EditionBase, db: Session = Depends(get_session)) return logic_editions.create_edition(db, edition) -@editions_router.delete("/{edition_name}", status_code=status.HTTP_204_NO_CONTENT, tags=[Tags.EDITIONS], +@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. diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index 9a0bd658d..fbdd55bda 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -14,7 +14,6 @@ async def get_students(edition_id: int): """ - @students_router.post("/emails") async def send_emails(edition_id: int): """ diff --git a/backend/src/app/routers/login/login.py b/backend/src/app/routers/login/login.py index 5f4b7afc0..ba6faea03 100644 --- a/backend/src/app/routers/login/login.py +++ b/backend/src/app/routers/login/login.py @@ -8,8 +8,8 @@ import settings from src.app.exceptions.authentication import InvalidCredentialsException -from src.app.logic.users import get_user_editions from src.app.logic.security import authenticate_user, create_access_token +from src.app.logic.users import get_user_editions from src.app.routers.tags import Tags from src.app.schemas.login import Token, UserData from src.database.database import get_session diff --git a/backend/src/app/routers/skills/skills.py b/backend/src/app/routers/skills/skills.py index c43cc40c3..2da8892b5 100644 --- a/backend/src/app/routers/skills/skills.py +++ b/backend/src/app/routers/skills/skills.py @@ -24,7 +24,7 @@ async def get_skills(db: Session = Depends(get_session)): return logic_skills.get_skills(db) -@skills_router.post("/",status_code=status.HTTP_201_CREATED, response_model=Skill, tags=[Tags.SKILLS], +@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. diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index 2b246cc0d..9d1a8d19d 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -1,8 +1,8 @@ from fastapi import APIRouter, Query, Depends from sqlalchemy.orm import Session -from src.app.routers.tags import Tags import src.app.logic.users as logic +from src.app.routers.tags import Tags from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, User as UserSchema from src.app.utils.dependencies import require_admin, get_current_active_user from src.database.database import get_session @@ -12,11 +12,11 @@ @users_router.get("/", response_model=UsersListResponse, dependencies=[Depends(require_admin)]) -async def get_users(admin: bool = Query(False), edition: str | None = Query(None), page: int = 0, db: Session = Depends(get_session)): +async def get_users(admin: bool = Query(False), edition: str | None = Query(None), page: int = 0, + db: Session = Depends(get_session)): """ Get users """ - return logic.get_users_list(db, admin, edition, page) @@ -31,7 +31,6 @@ async def patch_admin_status(user_id: int, admin: AdminPatch, db: Session = Depe """ Set admin-status of user """ - logic.edit_admin_status(db, user_id, admin) @@ -40,7 +39,6 @@ async def add_to_edition(user_id: int, edition_name: str, db: Session = Depends( """ Add user as coach of the given edition """ - logic.add_coach(db, user_id, edition_name) @@ -49,7 +47,6 @@ async def remove_from_edition(user_id: int, edition_name: str, db: Session = Dep """ Remove user as coach of the given edition """ - logic.remove_coach(db, user_id, edition_name) @@ -58,7 +55,6 @@ async def remove_from_all_editions(user_id: int, db: Session = Depends(get_sessi """ Remove user as coach from all editions """ - logic.remove_coach_all_editions(db, user_id) @@ -67,7 +63,6 @@ async def get_requests(edition: str | None = Query(None), page: int = 0, db: Ses """ Get pending userrequests """ - return logic.get_request_list(db, edition, page) @@ -76,7 +71,6 @@ async def accept_request(request_id: int, db: Session = Depends(get_session)): """ Accept a coach request """ - logic.accept_request(db, request_id) @@ -85,5 +79,4 @@ async def reject_request(request_id: int, db: Session = Depends(get_session)): """ Reject a coach request """ - logic.reject_request(db, request_id) diff --git a/backend/src/app/schemas/invites.py b/backend/src/app/schemas/invites.py index cdea6258f..ae6f53639 100644 --- a/backend/src/app/schemas/invites.py +++ b/backend/src/app/schemas/invites.py @@ -2,8 +2,8 @@ from pydantic import Field, validator -from src.app.schemas.validators import validate_email_format from src.app.schemas.utils import CamelCaseModel +from src.app.schemas.validators import validate_email_format class EmailAddress(CamelCaseModel): diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index 8fe9009e5..a99185e42 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -1,7 +1,6 @@ from pydantic import BaseModel from src.app.schemas.utils import CamelCaseModel -from src.database.enums import DecisionEnum class User(CamelCaseModel): diff --git a/backend/src/app/schemas/register.py b/backend/src/app/schemas/register.py index 59eaeb8b0..e41e3d111 100644 --- a/backend/src/app/schemas/register.py +++ b/backend/src/app/schemas/register.py @@ -1,4 +1,5 @@ from uuid import UUID + from src.app.schemas.invites import EmailAddress diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index 3b901b421..62678c42b 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -9,8 +9,8 @@ MissingPermissionsException from src.app.logic.security import ALGORITHM, get_user_by_id from src.database.crud.editions import get_edition_by_name -from src.database.crud.projects import db_get_project from src.database.crud.invites import get_invite_link_by_uuid +from src.database.crud.projects import db_get_project from src.database.database import get_session from src.database.models import Edition, InviteLink, User, Project diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index ee6fc650f..9a308c311 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -23,7 +23,7 @@ def db_add_project(db: Session, edition: Edition, input_project: InputProject) - """ 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 = [] diff --git a/backend/src/database/crud/projects_students.py b/backend/src/database/crud/projects_students.py index 5ba5a79e5..265b443b7 100644 --- a/backend/src/database/crud/projects_students.py +++ b/backend/src/database/crud/projects_students.py @@ -5,8 +5,8 @@ def db_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() + proj_role = db.query(ProjectRole).where( + ProjectRole.student_id == student_id).where(ProjectRole.project == project).one() db.delete(proj_role) db.commit() diff --git a/backend/src/database/crud/skills.py b/backend/src/database/crud/skills.py index f1592f4e2..b9d02a419 100644 --- a/backend/src/database/crud/skills.py +++ b/backend/src/database/crud/skills.py @@ -1,6 +1,7 @@ from sqlalchemy.orm import Session -from src.database.models import Skill + from src.app.schemas.skills import SkillBase +from src.database.models import Skill def get_skills(db: Session) -> list[Skill]: From 11dc018f213debd54185751518020c1d8729da70 Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 12:50:55 +0200 Subject: [PATCH 269/536] disable linting student routes --- backend/src/app/routers/editions/students/students.py | 1 + .../editions/students/suggestions/students_suggestions.py | 1 + 2 files changed, 2 insertions(+) diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index fbdd55bda..cc32e4158 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -1,3 +1,4 @@ +# pylint: skip-file from fastapi import APIRouter from src.app.routers.tags import Tags diff --git a/backend/src/app/routers/editions/students/suggestions/students_suggestions.py b/backend/src/app/routers/editions/students/suggestions/students_suggestions.py index aac361f44..8814b93e3 100644 --- a/backend/src/app/routers/editions/students/suggestions/students_suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/students_suggestions.py @@ -1,3 +1,4 @@ +# pylint: skip-file from fastapi import APIRouter from src.app.routers.tags import Tags From 2f7706a1b078f2d831f100519c8754322e1c7fcc Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 12:58:37 +0200 Subject: [PATCH 270/536] rename variable --- backend/src/app/schemas/editions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/app/schemas/editions.py b/backend/src/app/schemas/editions.py index 56f76e1a3..cc0efd5c7 100644 --- a/backend/src/app/schemas/editions.py +++ b/backend/src/app/schemas/editions.py @@ -10,10 +10,10 @@ class EditionBase(CamelCaseModel): year: int @validator("name") - def valid_format(cls, v): + def valid_format(cls, value): """Check that the email is of a valid format""" - validate_edition(v) - return v + validate_edition(value) + return value class Edition(CamelCaseModel): From 7651727a4cce2dcd81582f92c92097605428369c Mon Sep 17 00:00:00 2001 From: beguille Date: Fri, 8 Apr 2022 12:58:58 +0200 Subject: [PATCH 271/536] added auth and did requested changes --- backend/src/app/exceptions/projects.py | 2 +- backend/src/app/logic/projects_students.py | 7 + .../projects/students/projects_students.py | 15 +- backend/src/app/schemas/projects.py | 4 +- .../test_students/test_students.py | 241 ++++++++++-------- 5 files changed, 155 insertions(+), 114 deletions(-) diff --git a/backend/src/app/exceptions/projects.py b/backend/src/app/exceptions/projects.py index 5ae7702d3..9377d1a4c 100644 --- a/backend/src/app/exceptions/projects.py +++ b/backend/src/app/exceptions/projects.py @@ -6,5 +6,5 @@ class StudentInConflictException(Exception): class FailedToAddProjectRoleException(Exception): """ - Exception raised when a projct_role can't be added for some reason + Exception raised when a project_role can't be added for some reason """ diff --git a/backend/src/app/logic/projects_students.py b/backend/src/app/logic/projects_students.py index 430fc5987..04ebace09 100644 --- a/backend/src/app/logic/projects_students.py +++ b/backend/src/app/logic/projects_students.py @@ -24,6 +24,9 @@ def logic_add_student_project(db: Session, project: Project, student_id: int, sk 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: @@ -43,6 +46,10 @@ def logic_change_project_role(db: Session, project: Project, student_id: int, sk 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: 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 c0fe66e2c..2db46cef6 100644 --- a/backend/src/app/routers/editions/projects/students/projects_students.py +++ b/backend/src/app/routers/editions/projects/students/projects_students.py @@ -7,14 +7,15 @@ logic_change_project_role, logic_confirm_project_role from src.app.routers.tags import Tags from src.app.schemas.projects import InputStudentRole -from src.app.utils.dependencies import get_project, require_admin +from src.app.utils.dependencies import get_project, require_admin, require_coach from src.database.database import get_session -from src.database.models import Project +from src.database.models import Project, User 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) +@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)): """ @@ -25,22 +26,22 @@ async def remove_student_from_project(student_id: int, db: Session = Depends(get @project_students_router.patch("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response) async def change_project_role(student_id: int, input_sr: InputStudentRole, db: Session = Depends(get_session), - project: Project = Depends(get_project)): + project: Project = Depends(get_project), 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, input_sr.drafter_id) + logic_change_project_role(db, project, student_id, input_sr.skill_id, user.user_id) @project_students_router.post("/{student_id}", status_code=status.HTTP_201_CREATED, response_class=Response) async def add_student_to_project(student_id: int, input_sr: InputStudentRole, db: Session = Depends(get_session), - project: Project = Depends(get_project)): + project: Project = Depends(get_project), 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, input_sr.drafter_id) + 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, diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index 55905503d..2f494da13 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -105,8 +105,6 @@ class InputProject(BaseModel): coaches: list[int] -# TO DO: change drafter_id to current user with authentication class InputStudentRole(BaseModel): - """Used for creating/patching a student role (temporary until authentication is implemented)""" + """Used for creating/patching a student role""" skill_id: int - drafter_id: int 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 59053606b..6d2b869e0 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 @@ -16,11 +16,13 @@ def database_with_data(database_session: Session) -> Session: 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) - project1 = Project(name="project1", edition=edition, number_of_students=2, skills=[skill1, skill2, skill3, 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) @@ -37,18 +39,25 @@ def database_with_data(database_session: Session) -> Session: 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 @@ -60,227 +69,238 @@ def current_edition(database_with_data: Session) -> Edition: return database_with_data.query(Edition).all()[-1] -def test_add_student_project(database_with_data: Session, test_client: TestClient): +def test_add_student_project(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): """tests add a student to a project""" - resp = test_client.post( - "/editions/ed2022/projects/1/students/3", json={"skill_id": 3, "drafter_id": 1}) + 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 = test_client.get('/editions/ed2022/projects') + response2 = auth_client.get('/editions/ed2022/projects') json = response2.json() - assert len(json['projects'][0]['projectRoles']) == 3 - assert json['projects'][0]['projectRoles'][2]['skillId'] == 3 + 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, test_client: TestClient): +def test_add_ghost_student_project(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): """tests add 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 = test_client.get('/editions/ed2022/projects/1') + response = auth_client.get('/editions/ed2022/projects/1') json = response.json() - assert len(json['projectRoles']) == 2 + assert len(json['projectRoles']) == 3 - resp = test_client.post( - "/editions/ed2022/projects/1/students/10", json={"skill_id": 3, "drafter_id": 1}) + resp = auth_client.post( + "/editions/ed2022/projects/1/students/10", json={"skill_id": 3}) assert resp.status_code == status.HTTP_404_NOT_FOUND - response = test_client.get('/editions/ed2022/projects/1') + response = auth_client.get('/editions/ed2022/projects/1') json = response.json() - assert len(json['projectRoles']) == 2 + assert len(json['projectRoles']) == 3 -def test_add_student_project_non_existing_skill(database_with_data: Session, test_client: TestClient): +def test_add_student_project_non_existing_skill(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): """tests add 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 = test_client.get('/editions/ed2022/projects/1') - json = response.json() - assert len(json['projectRoles']) == 2 - - resp = test_client.post( - "/editions/ed2022/projects/1/students/3", json={"skill_id": 10, "drafter_id": 1}) - assert resp.status_code == status.HTTP_404_NOT_FOUND - - response = test_client.get('/editions/ed2022/projects/1') - json = response.json() - assert len(json['projectRoles']) == 2 - - -def test_add_student_project_ghost_drafter(database_with_data: Session, test_client: TestClient): - """test add a student to a project with a drafter that don't exist""" - user10: list[User] = database_with_data.query( - User).where(User.user_id == 10).all() - assert len(user10) == 0 - - response = test_client.get('/editions/ed2022/projects/1') + response = auth_client.get('/editions/ed2022/projects/1') json = response.json() - assert len(json['projectRoles']) == 2 + assert len(json['projectRoles']) == 3 - resp = test_client.post( - "/editions/ed2022/projects/1/students/3", json={"skill_id": 3, "drafter_id": 10}) + resp = auth_client.post( + "/editions/ed2022/projects/1/students/3", json={"skill_id": 10}) assert resp.status_code == status.HTTP_404_NOT_FOUND - response = test_client.get('/editions/ed2022/projects/1') + response = auth_client.get('/editions/ed2022/projects/1') json = response.json() - assert len(json['projectRoles']) == 2 + assert len(json['projectRoles']) == 3 -def test_add_student_to_ghost_project(database_with_data: Session, test_client: TestClient): +def test_add_student_to_ghost_project(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): """test add a student to a project that don'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 = test_client.post( - "/editions/ed2022/projects/10/students/1", json={"skill_id": 1, "drafter_id": 1}) + 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, test_client: TestClient): +def test_add_incomplete_data_student_project(database_session: Session, auth_client: AuthClient): """test add a student with incomplete data""" - database_session.add(Edition(year=2022, name="ed2022")) + + edition = Edition(year=2022, name="ed2022") + database_session.add(edition) project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) database_session.add(project) database_session.commit() - resp = test_client.post( - "/editions/ed2022/projects/1/students/1", json={"drafter_id": 1}) + 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 = test_client.get('/editions/ed2022/projects') + 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, test_client: TestClient): +def test_change_student_project(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): """test change a student project""" - resp1 = test_client.patch( - "/editions/ed2022/projects/1/students/1", json={"skill_id": 4, "drafter_id": 1}) + 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 = test_client.get('/editions/ed2022/projects') + response2 = auth_client.get('/editions/ed2022/projects') json = response2.json() - assert len(json['projects'][0]['projectRoles']) == 2 + 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, test_client: TestClient): +def test_change_incomplete_data_student_project(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): """test change student project with incomplete data""" - resp1 = test_client.patch( - "/editions/ed2022/projects/1/students/1", json={"skill_id": 2}) + 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 = test_client.get('/editions/ed2022/projects') + response2 = auth_client.get('/editions/ed2022/projects') json = response2.json() - assert len(json['projects'][0]['projectRoles']) == 2 + 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, test_client: TestClient): +def test_change_ghost_student_project(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): """tests change 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 = test_client.get('/editions/ed2022/projects/1') + response = auth_client.get('/editions/ed2022/projects/1') json = response.json() - assert len(json['projectRoles']) == 2 + assert len(json['projectRoles']) == 3 - resp = test_client.patch( - "/editions/ed2022/projects/1/students/10", json={"skill_id": 4, "drafter_id": 1}) + resp = auth_client.patch( + "/editions/ed2022/projects/1/students/10", json={"skill_id": 4}) assert resp.status_code == status.HTTP_404_NOT_FOUND - response = test_client.get('/editions/ed2022/projects/1') + response = auth_client.get('/editions/ed2022/projects/1') json = response.json() - assert len(json['projectRoles']) == 2 + assert len(json['projectRoles']) == 3 -def test_change_student_project_non_existing_skill(database_with_data: Session, test_client: TestClient): +def test_change_student_project_non_existing_skill(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): """test change a skill of a projectRole to a non-existing one""" + auth_client.coach(current_edition) + skill10: list[Skill] = database_with_data.query( Skill).where(Skill.skill_id == 10).all() assert len(skill10) == 0 - response = test_client.get('/editions/ed2022/projects/1') + response = auth_client.get('/editions/ed2022/projects/1') json = response.json() - assert len(json['projectRoles']) == 2 + assert len(json['projectRoles']) == 3 - resp = test_client.patch( - "/editions/ed2022/projects/1/students/3", json={"skill_id": 10, "drafter_id": 1}) + resp = auth_client.patch( + "/editions/ed2022/projects/1/students/3", json={"skill_id": 10}) assert resp.status_code == status.HTTP_404_NOT_FOUND - response = test_client.get('/editions/ed2022/projects/1') + response = auth_client.get('/editions/ed2022/projects/1') json = response.json() - assert len(json['projectRoles']) == 2 + assert len(json['projectRoles']) == 3 -def test_change_student_project_ghost_drafter(database_with_data: Session, test_client: TestClient): +def test_change_student_project_ghost_drafter(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): """test change 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 = test_client.get('/editions/ed2022/projects/1') + response = auth_client.get('/editions/ed2022/projects/1') json = response.json() - assert len(json['projectRoles']) == 2 + assert len(json['projectRoles']) == 3 - resp = test_client.patch( - "/editions/ed2022/projects/1/students/3", json={"skill_id": 4, "drafter_id": 10}) + resp = auth_client.patch( + "/editions/ed2022/projects/1/students/3", json={"skill_id": 4}) assert resp.status_code == status.HTTP_404_NOT_FOUND - response = test_client.get('/editions/ed2022/projects/1') + response = auth_client.get('/editions/ed2022/projects/1') json = response.json() - assert len(json['projectRoles']) == 2 + assert len(json['projectRoles']) == 3 -def test_change_student_to_ghost_project(database_with_data: Session, test_client: TestClient): +def test_change_student_to_ghost_project(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): """test change a student of a project that don'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 = test_client.patch( - "/editions/ed2022/projects/10/students/1", json={"skill_id": 1, "drafter_id": 1}) + 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, test_client: TestClient): +def test_delete_student_project(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): """test delete a student from a project""" - resp = test_client.delete("/editions/ed2022/projects/1/students/1") + auth_client.coach(current_edition) + + resp = auth_client.delete("/editions/ed2022/projects/1/students/1") assert resp.status_code == status.HTTP_204_NO_CONTENT - response2 = test_client.get('/editions/ed2022/projects') + response2 = auth_client.get('/editions/ed2022/projects') json = response2.json() - assert len(json['projects'][0]['projectRoles']) == 1 + assert len(json['projects'][0]['projectRoles']) == 2 + +def test_delete_student_project_empty(database_session: Session, auth_client: AuthClient): + """delete a student from a project that isn't assigned""" -def test_delete_student_project_empty(database_session: Session, test_client: TestClient): - """delete a student from a project that isn't asigned""" - database_session.add(Edition(year=2022, name="ed2022")) + edition = Edition(year=2022, name="ed2022") + database_session.add(edition) project = Project(name="project", edition_id=1, project_id=1, number_of_students=2) database_session.add(project) database_session.commit() - resp = test_client.delete("/editions/ed2022/projects/1/students/1") + auth_client.coach(edition) + resp = auth_client.delete("/editions/ed2022/projects/1/students/1") assert resp.status_code == status.HTTP_404_NOT_FOUND -def test_get_conflicts(database_with_data: Session, test_client: TestClient): +def test_get_conflicts(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): """test get the conflicts""" - response = test_client.get("/editions/ed2022/projects/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 @@ -288,38 +308,53 @@ def test_get_conflicts(database_with_data: Session, test_client: TestClient): assert json['editionName'] == "ed2022" -def test_add_student_same_project_role(database_with_data: Session, test_client: TestClient): +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""" - resp = test_client.post( - "/editions/ed2022/projects/1/students/3", json={"skill_id": 2, "drafter_id": 1}) + 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, test_client: TestClient): +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""" - resp = test_client.post( - "/editions/ed2022/projects/3/students/3", json={"skill_id": 4, "drafter_id": 1}) + 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, test_client: TestClient): +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""" - resp = test_client.post( - "/editions/ed2022/projects/1/students/2", json={"skill_id": 1, "drafter_id": 1}) + 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, "drafter_id": 1}) + "/editions/ed2022/projects/1/students/3", json={"skill_id": 3}) assert resp.status_code == status.HTTP_201_CREATED - auth_client.admin() response2 = auth_client.post( "/editions/ed2022/projects/1/students/3/confirm") From 09403b8e3ecab9c6b5989b1c2e87bb50eb587941 Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 14:13:19 +0200 Subject: [PATCH 272/536] fix linting a bit --- .../editions/projects/students/projects_students.py | 13 ++++++++----- backend/src/app/schemas/login.py | 1 + backend/src/database/crud/editions.py | 10 ++-------- backend/src/database/crud/invites.py | 4 +++- backend/src/database/crud/projects.py | 3 ++- backend/src/database/crud/users.py | 5 ++--- backend/src/database/crud/util.py | 1 + 7 files changed, 19 insertions(+), 18 deletions(-) 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 2a5db43ff..c64e3e7a3 100644 --- a/backend/src/app/routers/editions/projects/students/projects_students.py +++ b/backend/src/app/routers/editions/projects/students/projects_students.py @@ -3,18 +3,19 @@ from starlette import status from starlette.responses import Response +from src.app.logic.projects_students import logic_remove_student_project, logic_add_student_project, \ + logic_change_project_role from src.app.routers.tags import Tags from src.app.schemas.projects import InputStudentRole from src.app.utils.dependencies import get_project, require_coach from src.database.database import get_session from src.database.models import Project -from src.app.logic.projects_students import logic_remove_student_project, logic_add_student_project, \ - logic_change_project_role 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)]) +@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)): """ @@ -23,7 +24,8 @@ async def remove_student_from_project(student_id: int, db: Session = Depends(get logic_remove_student_project(db, project, student_id) -@project_students_router.patch("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, dependencies=[Depends(require_coach)]) +@project_students_router.patch("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, + dependencies=[Depends(require_coach)]) async def change_project_role(student_id: int, input_sr: InputStudentRole, db: Session = Depends(get_session), project: Project = Depends(get_project)): """ @@ -32,7 +34,8 @@ async def change_project_role(student_id: int, input_sr: InputStudentRole, db: S logic_change_project_role(db, project, student_id, input_sr.skill_id, input_sr.drafter_id) -@project_students_router.post("/{student_id}", status_code=status.HTTP_201_CREATED, response_class=Response, dependencies=[Depends(require_coach)]) +@project_students_router.post("/{student_id}", status_code=status.HTTP_201_CREATED, response_class=Response, + dependencies=[Depends(require_coach)]) async def add_student_to_project(student_id: int, input_sr: InputStudentRole, db: Session = Depends(get_session), project: Project = Depends(get_project)): """ diff --git a/backend/src/app/schemas/login.py b/backend/src/app/schemas/login.py index 02a6514ca..2a70ae514 100644 --- a/backend/src/app/schemas/login.py +++ b/backend/src/app/schemas/login.py @@ -9,6 +9,7 @@ class UserData(CamelCaseModel): editions: list[str] = [] class Config: + """The Model config""" orm_mode = True diff --git a/backend/src/database/crud/editions.py b/backend/src/database/crud/editions.py index 37e279320..861377e24 100644 --- a/backend/src/database/crud/editions.py +++ b/backend/src/database/crud/editions.py @@ -27,18 +27,12 @@ def _get_editions_query(db: Session) -> Query: def get_editions(db: Session) -> list[Edition]: - """Get a list of all editions. - - Args: - db (Session): connection with the database. - - Returns: - EditionList: an object with a list of all editions - """ + """Returns a list of all editions""" return _get_editions_query(db).all() def get_editions_page(db: Session, page: int) -> list[Edition]: + """Returns a paginated list of all editions""" return paginate(_get_editions_query(db), page).all() diff --git a/backend/src/database/crud/invites.py b/backend/src/database/crud/invites.py index 4ba7c9d7b..90fe70a4c 100644 --- a/backend/src/database/crud/invites.py +++ b/backend/src/database/crud/invites.py @@ -24,15 +24,17 @@ def delete_invite_link(db: Session, invite_link: InviteLink, commit: bool = True def _get_pending_invites_for_edition_query(db: Session, edition: Edition) -> Query: - """Return a list of all invite links in a given edition""" + """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) def get_pending_invites_for_edition(db: Session, 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() def get_pending_invites_for_edition_page(db: Session, 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() diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index 9a308c311..88308b347 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -11,11 +11,12 @@ def _get_projects_for_edition_query(db: Session, edition: Edition) -> Query: def db_get_projects_for_edition(db: Session, edition: Edition) -> list[Project]: - """Query all projects from a certain edition from the database""" + """Returns a list of all projects from a certain edition from the database""" return _get_projects_for_edition_query(db, edition).all() def db_get_projects_for_edition_page(db: Session, edition: Edition, page: int) -> list[Project]: + """Returns a paginated list of all projects from a certain edition from the database""" return paginate(_get_projects_for_edition_query(db, edition), page).all() diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index a9a4aeea2..05202739a 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -23,13 +23,12 @@ def _get_users_query(db: Session) -> Query: def get_users(db: Session) -> list[User]: - """ - Get all users (coaches + admins) - """ + """Get all users (coaches + admins)""" return _get_users_query(db).all() def get_users_page(db: Session, page: int) -> list[User]: + """Get all users (coaches + admins) paginated""" return paginate(_get_users_query(db), page).all() diff --git a/backend/src/database/crud/util.py b/backend/src/database/crud/util.py index 7ed40af75..ca4a97020 100644 --- a/backend/src/database/crud/util.py +++ b/backend/src/database/crud/util.py @@ -4,4 +4,5 @@ def paginate(query: Query, page: int) -> Query: + """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) From 81cc1e5372e01350ace6dd60ce2bdba3c6d7b7a8 Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 14:24:49 +0200 Subject: [PATCH 273/536] fix linting for validators --- backend/src/app/schemas/editions.py | 1 + backend/src/app/schemas/invites.py | 1 + 2 files changed, 2 insertions(+) diff --git a/backend/src/app/schemas/editions.py b/backend/src/app/schemas/editions.py index cc0efd5c7..d9cd01913 100644 --- a/backend/src/app/schemas/editions.py +++ b/backend/src/app/schemas/editions.py @@ -10,6 +10,7 @@ class EditionBase(CamelCaseModel): year: int @validator("name") + @classmethod def valid_format(cls, value): """Check that the email is of a valid format""" validate_edition(value) diff --git a/backend/src/app/schemas/invites.py b/backend/src/app/schemas/invites.py index ae6f53639..d3123f4fd 100644 --- a/backend/src/app/schemas/invites.py +++ b/backend/src/app/schemas/invites.py @@ -13,6 +13,7 @@ class EmailAddress(CamelCaseModel): email: str @validator("email") + @classmethod def valid_format(cls, validate): """Check that the email is of a valid format""" validate_email_format(validate) From 09f6b5d466fe4737803d7cd0fb07713143526ff0 Mon Sep 17 00:00:00 2001 From: beguille Date: Fri, 8 Apr 2022 15:00:30 +0200 Subject: [PATCH 274/536] comparing to boolean fixed --- backend/src/app/logic/projects_students.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/app/logic/projects_students.py b/backend/src/app/logic/projects_students.py index 04ebace09..5ad72460d 100644 --- a/backend/src/app/logic/projects_students.py +++ b/backend/src/app/logic/projects_students.py @@ -1,5 +1,4 @@ from sqlalchemy.orm import Session - from src.app.exceptions.projects import StudentInConflictException, FailedToAddProjectRoleException from src.app.logic.projects import logic_get_conflicts from src.app.schemas.projects import ConflictStudentList @@ -25,7 +24,7 @@ def logic_add_student_project(db: Session, project: Project, student_id: int, sk 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: + 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() @@ -48,7 +47,7 @@ def logic_change_project_role(db: Session, project: Project, student_id: int, sk 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: + 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() From 3109ee6c89ed7e45ff1373bf256238fe21260391 Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 15:22:06 +0200 Subject: [PATCH 275/536] add crud tests --- .../test_database/test_crud/test_invites.py | 22 +++++- .../test_database/test_crud/test_projects.py | 41 +++++++--- .../test_crud/test_projects_students.py | 3 +- .../test_database/test_crud/test_register.py | 8 +- .../test_database/test_crud/test_users.py | 75 ++++++++++++++++++- 5 files changed, 133 insertions(+), 16 deletions(-) diff --git a/backend/tests/test_database/test_crud/test_invites.py b/backend/tests/test_database/test_crud/test_invites.py index 18e54108d..7cead2745 100644 --- a/backend/tests/test_database/test_crud/test_invites.py +++ b/backend/tests/test_database/test_crud/test_invites.py @@ -4,9 +4,15 @@ import sqlalchemy.exc from sqlalchemy.orm import Session +from settings import DB_PAGE_SIZE from src.app.exceptions.parsing import MalformedUUIDError -from src.database.crud.invites import create_invite_link, delete_invite_link, get_pending_invites_for_edition, \ +from src.database.crud.invites import ( + create_invite_link, + delete_invite_link, + get_pending_invites_for_edition, + get_pending_invites_for_edition_page, get_invite_link_by_uuid +) from src.database.models import Edition, InviteLink @@ -93,6 +99,20 @@ def test_get_all_pending_invites_two_present(database_session: Session): assert len(get_pending_invites_for_edition(database_session, edition_two)) == 1 +def test_get_all_pending_invites_pagination(database_session: Session): + """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() + + 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( + DB_PAGE_SIZE * 1.5 + ) - DB_PAGE_SIZE + + def test_get_invite_link_by_uuid_existing(database_session: Session): """Test fetching links by uuid's when it exists""" edition = Edition(year=2022, name="ed2022") diff --git a/backend/tests/test_database/test_crud/test_projects.py b/backend/tests/test_database/test_crud/test_projects.py index 2762dcbc0..dee67b51e 100644 --- a/backend/tests/test_database/test_crud/test_projects.py +++ b/backend/tests/test_database/test_crud/test_projects.py @@ -1,11 +1,18 @@ import pytest -from sqlalchemy.orm import Session from sqlalchemy.exc import NoResultFound +from sqlalchemy.orm import Session -from src.app.schemas.projects import ConflictStudent, InputProject -from src.database.crud.projects import (db_get_projects_for_edition, db_add_project, - db_get_project, db_delete_project, - db_patch_project, db_get_conflict_students) +from settings import DB_PAGE_SIZE +from src.app.schemas.projects import InputProject +from src.database.crud.projects import ( + db_get_projects_for_edition, + db_get_projects_for_edition_page, + db_add_project, + db_get_project, + db_delete_project, + db_patch_project, + db_get_conflict_students +) from src.database.models import Edition, Partner, Project, User, Skill, ProjectRole, Student @@ -66,11 +73,25 @@ def test_get_all_projects_empty(database_session: Session): def test_get_all_projects(database_with_data: Session, current_edition: Edition): """test get all projects""" - projects: list[Project] = db_get_projects_for_edition( - database_with_data, current_edition) + projects: list[Project] = db_get_projects_for_edition(database_with_data, current_edition) assert len(projects) == 3 +def test_get_all_projects_pagination(database_session: Session): + """test get all projects""" + 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() + + assert len(db_get_projects_for_edition_page(database_session, edition, 0)) == DB_PAGE_SIZE + assert len(db_get_projects_for_edition_page(database_session, edition, 1)) == round( + DB_PAGE_SIZE * 1.5 + ) - DB_PAGE_SIZE + + def test_add_project_partner_do_not_exist_yet(database_with_data: Session, current_edition: Edition): """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], @@ -98,7 +119,7 @@ def test_add_project_partner_do_not_exist_yet(database_with_data: Session, curre 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]) + partners=["ugent"], coaches=[1]) database_with_data.add(Partner(name="ugent")) assert len(database_with_data.query(Partner).where( Partner.name == "ugent").all()) == 1 @@ -160,9 +181,9 @@ def test_delete_project_with_project_roles(database_with_data: Session, current_ def test_patch_project(database_with_data: Session, current_edition: Edition): """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]) + 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 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 253988fa1..a71ca4a52 100644 --- a/backend/tests/test_database/test_crud/test_projects_students.py +++ b/backend/tests/test_database/test_crud/test_projects_students.py @@ -1,6 +1,6 @@ import pytest -from sqlalchemy.orm import Session from sqlalchemy.exc import NoResultFound +from sqlalchemy.orm import Session from src.database.crud.projects_students import ( db_remove_student_project, db_add_student_project, db_change_project_role) @@ -88,6 +88,7 @@ def test_change_project_role(database_with_data: Session): db_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( diff --git a/backend/tests/test_database/test_crud/test_register.py b/backend/tests/test_database/test_crud/test_register.py index 67decaba0..b4f471b6c 100644 --- a/backend/tests/test_database/test_crud/test_register.py +++ b/backend/tests/test_database/test_crud/test_register.py @@ -1,8 +1,8 @@ from sqlalchemy.orm import Session +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.database.crud.register import create_user, create_coach_request, create_auth_email def test_create_user(database_session: Session): """Tests for creating a user""" @@ -12,6 +12,7 @@ def test_create_user(database_session: Session): assert len(a) == 1 assert a[0].name == "jos" + def test_react_coach_request(database_session: Session): """Tests for creating a coach request""" edition = Edition(year=2022, name="ed2022") @@ -22,16 +23,17 @@ def test_react_coach_request(database_session: Session): a = database_session.query(CoachRequest).where(CoachRequest.user == u).all() assert len(a) == 1 - assert a[0].user_id == u.user_id + assert a[0].user_id == u.user_id assert u.coach_request == a[0] + def test_create_auth_email(database_session: Session): """Tests for creating a auth email""" u = create_user(database_session, "jos") create_auth_email(database_session, u, "wachtwoord", "mail@email.com") a = database_session.query(AuthEmail).where(AuthEmail.user == u).all() - + assert len(a) == 1 assert a[0].user_id == u.user_id assert a[0].pw_hash == "wachtwoord" diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index 4f789f793..1c09e27b6 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -1,8 +1,9 @@ import pytest from sqlalchemy.orm import Session -from src.database import models import src.database.crud.users as users_crud +from settings import DB_PAGE_SIZE +from src.database import models from src.database.models import user_editions, CoachRequest @@ -56,6 +57,17 @@ def test_get_all_users(database_session: Session, data: dict[str, int]): assert data["user2"] in user_ids +def test_get_all_users_paginated(database_session: Session): + for i in range(round(DB_PAGE_SIZE * 1.5)): + database_session.add(models.User(name=f"Project {i}", admin=False)) + database_session.commit() + + assert len(users_crud.get_users_page(database_session, 0)) == DB_PAGE_SIZE + assert len(users_crud.get_users_page(database_session, 1)) == round( + DB_PAGE_SIZE * 1.5 + ) - DB_PAGE_SIZE + + def test_get_all_admins(database_session: Session, data: dict[str, str]): """Test get request for admins""" @@ -113,6 +125,35 @@ def test_get_all_users_from_edition(database_session: Session, data: dict[str, s assert data["user2"] == users[0].user_id +def test_get_all_users_for_edition_paginated(database_session: Session): + 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() + + 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(), [ + {"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() + + assert len(users_crud.get_users_for_edition_page(database_session, edition_1.name, 0)) == DB_PAGE_SIZE + assert len(users_crud.get_users_for_edition_page(database_session, edition_1.name, 1)) == round( + DB_PAGE_SIZE * 1.5 + ) - DB_PAGE_SIZE + assert len(users_crud.get_users_for_edition_page(database_session, edition_2.name, 0)) == DB_PAGE_SIZE + assert len(users_crud.get_users_for_edition_page(database_session, edition_2.name, 1)) == round( + DB_PAGE_SIZE * 1.5 + ) - DB_PAGE_SIZE + + def test_get_admins_from_edition(database_session: Session, data: dict[str, str]): """Test get request for admins of a given edition""" @@ -248,6 +289,22 @@ def test_get_all_requests(database_session: Session): assert user2 in users +def test_get_requests_paginated(database_session: Session): + edition = models.Edition(year=2022, name="ed2022") + database_session.add(edition) + + for i in range(round(DB_PAGE_SIZE * 1.5)): + user = models.User(name=f"User {i}", admin=False) + database_session.add(user) + database_session.add(CoachRequest(user=user, edition=edition)) + 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( + DB_PAGE_SIZE * 1.5 + ) - DB_PAGE_SIZE + + def test_get_all_requests_from_edition(database_session: Session): """Test get request for all userrequests of a given edition""" @@ -282,6 +339,22 @@ def test_get_all_requests_from_edition(database_session: Session): assert requests[0].user == user2 +def test_get_requests_for_edition_paginated(database_session: Session): + edition = models.Edition(year=2022, name="ed2022") + database_session.add(edition) + + for i in range(round(DB_PAGE_SIZE * 1.5)): + user = models.User(name=f"User {i}", admin=False) + database_session.add(user) + database_session.add(CoachRequest(user=user, edition=edition)) + 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( + DB_PAGE_SIZE * 1.5 + ) - DB_PAGE_SIZE + + def test_accept_request(database_session: Session): """Test accepting a coach request""" From 4215cc85e1332fe98c712ac6a7a5ec1b21b498cb Mon Sep 17 00:00:00 2001 From: beguille Date: Fri, 1 Apr 2022 16:30:42 +0200 Subject: [PATCH 276/536] #135, made exception for read-only edition, latest edition dependency --- backend/src/app/exceptions/editions.py | 2 ++ backend/src/app/exceptions/handlers.py | 9 ++++++++- backend/src/app/utils/dependencies.py | 7 ++++++- backend/src/database/crud/editions.py | 5 +++++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/backend/src/app/exceptions/editions.py b/backend/src/app/exceptions/editions.py index 8221e908e..c61f8d3fe 100644 --- a/backend/src/app/exceptions/editions.py +++ b/backend/src/app/exceptions/editions.py @@ -4,3 +4,5 @@ class DuplicateInsertException(Exception): 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 1f7c10327..30540da38 100644 --- a/backend/src/app/exceptions/handlers.py +++ b/backend/src/app/exceptions/handlers.py @@ -5,7 +5,7 @@ from starlette import status from .authentication import ExpiredCredentialsException, InvalidCredentialsException, MissingPermissionsException -from .editions import DuplicateInsertException +from .editions import DuplicateInsertException, ReadOnlyEditionException from .parsing import MalformedUUIDError from .projects import StudentInConflictException, FailedToAddProjectRoleException from .register import FailedToAddNewUserException @@ -94,3 +94,10 @@ def student_in_conflict_exception(_request: Request, _exception: FailedToAddProj status_code=status.HTTP_400_BAD_REQUEST, content={'message': 'Something went wrong while adding this student to the project'} ) + + @app.exception_handler(ReadOnlyEditionException) + def failed_to_add_new_user_exception(_request: Request, _exception: ReadOnlyEditionException): + return JSONResponse( + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, + content={'message': 'This edition is Read-Only'} + ) diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index 3b901b421..f9ffbfa23 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -8,7 +8,7 @@ from src.app.exceptions.authentication import ExpiredCredentialsException, InvalidCredentialsException, \ MissingPermissionsException from src.app.logic.security import ALGORITHM, get_user_by_id -from src.database.crud.editions import get_edition_by_name +from src.database.crud.editions import get_edition_by_name, latest_edition from src.database.crud.projects import db_get_project from src.database.crud.invites import get_invite_link_by_uuid from src.database.database import get_session @@ -20,6 +20,11 @@ def get_edition(edition_name: str, database: Session = Depends(get_session)) -> return get_edition_by_name(database, edition_name) +def get_latest_edition(database: Session = Depends(get_session)) -> Edition: + """Get the latest edition to verify if it can be modified""" + return latest_edition(database) + + oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login/token") diff --git a/backend/src/database/crud/editions.py b/backend/src/database/crud/editions.py index 6532c185b..00a810a44 100644 --- a/backend/src/database/crud/editions.py +++ b/backend/src/database/crud/editions.py @@ -61,3 +61,8 @@ def delete_edition(db: Session, edition_name: str): edition_to_delete = get_edition_by_name(db, edition_name) db.delete(edition_to_delete) db.commit() + + +def latest_edition(db: Session) -> Edition: + """Returns the latest edition from the database""" + return db.query(Edition).all()[-1] From 9e67e335ee8dc558800eeef2051cdfbee398822f Mon Sep 17 00:00:00 2001 From: beguille Date: Sat, 2 Apr 2022 10:25:49 +0200 Subject: [PATCH 277/536] closes #135, added readonly checks to necessary routes --- backend/src/app/logic/invites.py | 3 +++ backend/src/app/logic/projects.py | 11 +++++++++-- backend/src/app/logic/projects_students.py | 18 ++++++++++++++---- backend/src/app/logic/register.py | 3 +++ .../app/routers/editions/projects/projects.py | 9 +++++---- .../projects/students/projects_students.py | 18 ++++++++++-------- backend/src/app/utils/dependencies.py | 5 ----- backend/src/app/utils/edition_readonly.py | 11 +++++++++++ backend/src/database/crud/webhooks.py | 3 +++ .../test_editions/test_invites/test_invites.py | 14 ++++++++++++++ .../test_projects/test_projects.py | 14 ++++++++++++++ .../test_students/test_students.py | 17 ++++++++++++++--- .../test_register/test_register.py | 16 ++++++++++++++++ .../test_webhooks/test_webhooks.py | 9 +++++++++ 14 files changed, 125 insertions(+), 26 deletions(-) create mode 100644 backend/src/app/utils/edition_readonly.py diff --git a/backend/src/app/logic/invites.py b/backend/src/app/logic/invites.py index b5b98abdc..04d619116 100644 --- a/backend/src/app/logic/invites.py +++ b/backend/src/app/logic/invites.py @@ -1,6 +1,7 @@ from sqlalchemy.orm import Session from src.app.schemas.invites import InvitesListResponse, EmailAddress, NewInviteLink, InviteLink as InviteLinkModel +from src.app.utils.edition_readonly import check_readonly_edition from src.app.utils.mailto import generate_mailto_string from src.database.crud.invites import create_invite_link, delete_invite_link as delete_link_db, get_all_pending_invites from src.database.models import Edition, InviteLink as InviteLinkDB @@ -29,6 +30,8 @@ def get_pending_invites_list(db: Session, edition: Edition) -> InvitesListRespon def create_mailto_link(db: Session, edition: Edition, email_address: EmailAddress) -> NewInviteLink: """Add a new invite link into the database & return a mailto link for it""" + check_readonly_edition(db, edition) + # Create db entry new_link_db = create_invite_link(db, edition, email_address.email) diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index 6d51d102a..9a14e0c6e 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -2,6 +2,7 @@ from src.app.schemas.projects import ProjectList, Project, ConflictStudentList, InputProject, Student, \ ConflictStudent, ConflictProject +from src.app.utils.edition_readonly import check_readonly_edition from src.database.crud.projects import db_get_all_projects, db_add_project, db_delete_project, \ db_patch_project, db_get_conflict_students from src.database.models import Edition, Project as ProjectModel @@ -22,19 +23,25 @@ def logic_get_project_list(db: Session, edition: Edition) -> ProjectList: def logic_create_project(db: Session, edition: Edition, input_project: InputProject) -> Project: """Create a new project""" + check_readonly_edition(db, edition) + project = db_add_project(db, edition, input_project) return Project(project_id=project.project_id, name=project.name, number_of_students=project.number_of_students, edition_name=project.edition.name, coaches=project.coaches, skills=project.skills, partners=project.partners, project_roles=project.project_roles) -def logic_delete_project(db: Session, project_id: int): +def logic_delete_project(db: Session, project_id: int, edition: Edition): """Delete a project""" + check_readonly_edition(db, edition) + db_delete_project(db, project_id) -def logic_patch_project(db: Session, project_id: int, input_project: InputProject): +def logic_patch_project(db: Session, project_id: int, input_project: InputProject, edition: Edition): """Make changes to a project""" + check_readonly_edition(db, edition) + db_patch_project(db, project_id, input_project) diff --git a/backend/src/app/logic/projects_students.py b/backend/src/app/logic/projects_students.py index 5ad72460d..5d040aa39 100644 --- a/backend/src/app/logic/projects_students.py +++ b/backend/src/app/logic/projects_students.py @@ -2,18 +2,25 @@ from src.app.exceptions.projects import StudentInConflictException, FailedToAddProjectRoleException from src.app.logic.projects import logic_get_conflicts from src.app.schemas.projects import ConflictStudentList + +from src.app.utils.edition_readonly import check_readonly_edition from src.database.crud.projects_students import db_remove_student_project, db_add_student_project, \ db_change_project_role, db_confirm_project_role -from src.database.models import Project, ProjectRole, Student, Skill +from src.database.models import Project, ProjectRole, Student, Skill, Edition -def logic_remove_student_project(db: Session, project: Project, student_id: int): +def logic_remove_student_project(db: Session, project: Project, student_id: int, edition: Edition): """Remove a student from a project""" + check_readonly_edition(db, edition) + db_remove_student_project(db, project, student_id) -def logic_add_student_project(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): +def logic_add_student_project(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int, + edition: Edition): """Add a student to a project""" + check_readonly_edition(db, edition) + # 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: @@ -34,8 +41,11 @@ def logic_add_student_project(db: Session, project: Project, student_id: int, sk db_add_student_project(db, project, student_id, skill_id, drafter_id) -def logic_change_project_role(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): +def logic_change_project_role(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int, + edition: Edition): """Change the role of the student in the project""" + check_readonly_edition(db, edition) + # 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: diff --git a/backend/src/app/logic/register.py b/backend/src/app/logic/register.py index 9a1a5c3bb..71421e498 100644 --- a/backend/src/app/logic/register.py +++ b/backend/src/app/logic/register.py @@ -2,6 +2,7 @@ from sqlalchemy.orm import Session from src.app.schemas.register import NewUser +from src.app.utils.edition_readonly import check_readonly_edition from src.database.models import Edition, InviteLink from src.database.crud.register import create_coach_request, create_user, create_auth_email from src.database.crud.invites import get_invite_link_by_uuid, delete_invite_link @@ -11,6 +12,8 @@ def create_request(db: Session, new_user: NewUser, edition: Edition) -> None: """Create a coach request. If something fails, the changes aren't committed""" + check_readonly_edition(db, edition) + invite_link: InviteLink = get_invite_link_by_uuid(db, new_user.uuid) with db.begin_nested() as transaction: diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index e7b12712d..2113ab581 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -47,11 +47,11 @@ async def get_conflicts(db: Session = Depends(get_session), edition: Edition = D @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)): +async def delete_project(project_id: int, db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): """ Delete a specific project. """ - return logic_delete_project(db, project_id) + return logic_delete_project(db, project_id, edition) @projects_router.get("/{project_id}", status_code=status.HTTP_200_OK, response_model=Project, @@ -69,8 +69,9 @@ async def get_project_route(project: ProjectModel = Depends(get_project)): @projects_router.patch("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, dependencies=[Depends(require_admin)]) -async def patch_project(project_id: int, input_project: InputProject, db: Session = Depends(get_session)): +async def patch_project(project_id: int, input_project: InputProject, db: Session = Depends(get_session), + edition: Edition = Depends(get_edition)): """ Update a project, changing some fields. """ - logic_patch_project(db, project_id, input_project) + logic_patch_project(db, project_id, input_project, edition) 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 1b402df4b..4a0bf76bb 100644 --- a/backend/src/app/routers/editions/projects/students/projects_students.py +++ b/backend/src/app/routers/editions/projects/students/projects_students.py @@ -7,9 +7,9 @@ logic_change_project_role, logic_confirm_project_role 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 +from src.app.utils.dependencies import get_project, require_admin, require_coach, get_edition from src.database.database import get_session -from src.database.models import Project, User +from src.database.models import Project, User, Edition project_students_router = APIRouter(prefix="/students", tags=[Tags.PROJECTS, Tags.STUDENTS]) @@ -17,31 +17,33 @@ @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: Project = Depends(get_project), edition: Edition = Depends(get_edition)): """ Remove a student from a project. """ - logic_remove_student_project(db, project, student_id) + logic_remove_student_project(db, project, student_id, edition) @project_students_router.patch("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, dependencies=[Depends(require_coach)]) 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: Project = Depends(get_project), user: User = Depends(require_coach), + edition: Edition = Depends(get_edition)): """ 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) + logic_change_project_role(db, project, student_id, input_sr.skill_id, user.user_id, edition) @project_students_router.post("/{student_id}", status_code=status.HTTP_201_CREATED, response_class=Response, dependencies=[Depends(require_coach)]) 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: Project = Depends(get_project), user: User = Depends(require_coach), + edition: Edition = Depends(get_edition)): """ 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) + logic_add_student_project(db, project, student_id, input_sr.skill_id, user.user_id, edition) @project_students_router.post("/{student_id}/confirm", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index f9ffbfa23..05ac8628e 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -20,11 +20,6 @@ def get_edition(edition_name: str, database: Session = Depends(get_session)) -> return get_edition_by_name(database, edition_name) -def get_latest_edition(database: Session = Depends(get_session)) -> Edition: - """Get the latest edition to verify if it can be modified""" - return latest_edition(database) - - oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login/token") diff --git a/backend/src/app/utils/edition_readonly.py b/backend/src/app/utils/edition_readonly.py new file mode 100644 index 000000000..3f1386ed3 --- /dev/null +++ b/backend/src/app/utils/edition_readonly.py @@ -0,0 +1,11 @@ +from sqlalchemy.orm import Session + +from src.app.exceptions.editions import ReadOnlyEditionException +from src.database.crud.editions import latest_edition +from src.database.models import Edition + + +def check_readonly_edition(db: Session, edition: Edition): + latest = latest_edition(db) + if edition != latest: + raise ReadOnlyEditionException diff --git a/backend/src/database/crud/webhooks.py b/backend/src/database/crud/webhooks.py index f2e25e98b..c0e7a9d30 100644 --- a/backend/src/database/crud/webhooks.py +++ b/backend/src/database/crud/webhooks.py @@ -1,5 +1,6 @@ from sqlalchemy.orm import Session +from src.app.utils.edition_readonly import check_readonly_edition from src.database.models import WebhookURL, Edition @@ -10,6 +11,8 @@ def get_webhook(database: Session, uuid: str) -> WebhookURL: def create_webhook(database: Session, edition: Edition) -> WebhookURL: """Create a webhook for a given edition""" + check_readonly_edition(database, edition) + webhook_url: WebhookURL = WebhookURL(edition=edition) database.add(webhook_url) database.commit() 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 4f06741e4..4870397c5 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 @@ -149,3 +149,17 @@ def test_get_invite_present(database_session: Session, auth_client: AuthClient): 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): + """Test endpoint for creating invites when data is valid, but the edition is read-only""" + auth_client.admin() + edition = Edition(year=2022, name="ed2022") + edition2 = Edition(year=2023, name="ed2023") + database_session.add(edition) + database_session.add(edition2) + 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_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 f5f8978ac..3fcb85f82 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 @@ -273,3 +273,17 @@ def test_patch_wrong_project(database_session: Session, auth_client: AuthClient) assert len(json['projects']) == 1 assert json['projects'][0]['name'] == 'project' + + +def test_create_project_old_edition(database_with_data: Session, test_client: TestClient): + """test create a project for a readonly edition""" + database_with_data.add(Edition(year=2023, name="ed2023")) + database_with_data.commit() + + response = \ + test_client.post("/editions/ed2022/projects/", + json={"name": "test", + "number_of_students": 5, + "skills": [1, 1, 1, 1, 1], "partners": ["ugent"], "coaches": [1]}) + + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED 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 38bb32f32..eaa38fd5f 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 @@ -75,7 +75,7 @@ def test_add_student_project(database_with_data: Session, current_edition: Editi 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') @@ -190,7 +190,7 @@ def test_change_incomplete_data_student_project(database_with_data: Session, cur 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): """Tests changing a non-existing student of a project""" auth_client.coach(current_edition) @@ -251,7 +251,7 @@ def test_change_student_project_ghost_drafter(database_with_data: Session, curre 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) @@ -304,6 +304,17 @@ def test_get_conflicts(database_with_data: Session, current_edition: Edition, au assert json['editionName'] == "ed2022" +def test_add_student_project_old_edition(database_with_data: Session, test_client: TestClient): + """tests add a student to a project from an old edition""" + database_with_data.add(Edition(year=2023, name="ed2023")) + database_with_data.commit() + + resp = test_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) 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 850a5ddb3..6d75d523c 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 @@ -89,3 +89,19 @@ def test_duplicate_user(database_session: Session, test_client: TestClient): "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): + """Tests trying to make a registration for a read-only edition""" + edition: Edition = Edition(year=2022, name="ed2022") + 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)}) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED 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 d82c19f75..a75d0a789 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 @@ -112,3 +112,12 @@ def test_webhook_missing_question(test_client: TestClient, webhook: WebhookURL, 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 From 4bfe4b2d71884d7852bedeebbe26da80f698da84 Mon Sep 17 00:00:00 2001 From: beguille Date: Sat, 2 Apr 2022 11:27:21 +0200 Subject: [PATCH 278/536] closes #184, coaches can only see their own editions --- backend/src/app/routers/editions/editions.py | 23 ++++++++---- .../test_editions/test_editions.py | 36 +++++++++++++++++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/backend/src/app/routers/editions/editions.py b/backend/src/app/routers/editions/editions.py index 5c569b948..6eb535e08 100644 --- a/backend/src/app/routers/editions/editions.py +++ b/backend/src/app/routers/editions/editions.py @@ -7,6 +7,7 @@ from src.database.database import get_session from src.app.logic import editions as logic_editions +from src.database.models import User from .invites import invites_router from .projects import projects_router @@ -16,7 +17,8 @@ # Don't add the "Editions" tag here, because then it gets applied # to all child routes as well -from ...utils.dependencies import require_admin, require_auth, require_coach +from ...exceptions.authentication import MissingPermissionsException +from ...utils.dependencies import require_admin, require_auth, require_coach, get_current_active_user editions_router = APIRouter(prefix="/editions") @@ -34,31 +36,40 @@ @editions_router.get("/", response_model=EditionList, tags=[Tags.EDITIONS], dependencies=[Depends(require_auth)]) -async def get_editions(db: Session = Depends(get_session)): +async def get_editions(db: Session = Depends(get_session), user: User = Depends(get_current_active_user)): """Get a 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(get_current_active_user). Returns: EditionList: an object with a list of all the editions. """ - # TODO only return editions the user can see - return logic_editions.get_editions(db) + if user.admin: + return logic_editions.get_editions(db) + else: + 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)): +async def get_edition_by_name(edition_name: str, db: Session = Depends(get_session), + user: User = Depends(get_current_active_user)): """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) + edition = logic_editions.get_edition_by_name(db, edition_name) + if not user.admin and edition not in user.editions: + raise MissingPermissionsException + + return edition @editions_router.post("/", status_code=status.HTTP_201_CREATED, response_model=Edition, tags=[Tags.EDITIONS], 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 260477e89..f71558ffd 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 @@ -183,3 +183,39 @@ def test_delete_edition_non_existing(database_session: Session, auth_client: Aut response = 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): + """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() + + auth_client.coach(edition) + + # Make the get request + response = auth_client.get("/editions/") + + assert response.status_code == status.HTTP_200_OK + response = response.json() + assert response["editions"][0]["year"] == 2022 + assert response["editions"][0]["editionId"] == 1 + assert response["editions"][0]["name"] == "ed2022" + assert len(response["editions"]) == 1 + + +def test_get_edition_by_name_coach_not_assigned(database_session: Session, 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() + + auth_client.coach(edition) + + # Make the get request + response = auth_client.get(f"/editions/{edition2.name}") + assert response.status_code == status.HTTP_403_FORBIDDEN From c30f09b7f12559d20954566472163eac76f5d20d Mon Sep 17 00:00:00 2001 From: beguille Date: Fri, 8 Apr 2022 14:53:43 +0200 Subject: [PATCH 279/536] fixed requested changes --- backend/src/app/exceptions/handlers.py | 2 +- backend/src/app/routers/editions/editions.py | 26 +++++++------------- backend/src/app/utils/edition_readonly.py | 1 + backend/src/database/crud/editions.py | 5 ++-- 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/backend/src/app/exceptions/handlers.py b/backend/src/app/exceptions/handlers.py index 30540da38..7b7c3245a 100644 --- a/backend/src/app/exceptions/handlers.py +++ b/backend/src/app/exceptions/handlers.py @@ -96,7 +96,7 @@ def student_in_conflict_exception(_request: Request, _exception: FailedToAddProj ) @app.exception_handler(ReadOnlyEditionException) - def failed_to_add_new_user_exception(_request: Request, _exception: ReadOnlyEditionException): + def read_only_edition_exception(_request: Request, _exception: ReadOnlyEditionException): return JSONResponse( status_code=status.HTTP_405_METHOD_NOT_ALLOWED, content={'message': 'This edition is Read-Only'} diff --git a/backend/src/app/routers/editions/editions.py b/backend/src/app/routers/editions/editions.py index 6eb535e08..0312d8761 100644 --- a/backend/src/app/routers/editions/editions.py +++ b/backend/src/app/routers/editions/editions.py @@ -1,25 +1,22 @@ from fastapi import APIRouter, Depends -from starlette import status from sqlalchemy.orm import Session +from starlette import status +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.database.database import get_session -from src.app.logic import editions as logic_editions from src.database.models import User - 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 - -# Don't add the "Editions" tag here, because then it gets applied -# to all child routes as well from ...exceptions.authentication import MissingPermissionsException from ...utils.dependencies import require_admin, require_auth, require_coach, get_current_active_user +# Don't add the "Editions" tag here, because then it gets applied +# to all child routes as well editions_router = APIRouter(prefix="/editions") # Register all child routers @@ -35,8 +32,8 @@ editions_router.include_router(router, prefix="/{edition_name}") -@editions_router.get("/", response_model=EditionList, tags=[Tags.EDITIONS], dependencies=[Depends(require_auth)]) -async def get_editions(db: Session = Depends(get_session), user: User = Depends(get_current_active_user)): +@editions_router.get("/", response_model=EditionList, tags=[Tags.EDITIONS]) +async def get_editions(db: Session = Depends(get_session), user: User = Depends(require_auth)): """Get a list of all editions. Args: db (Session, optional): connection with the database. Defaults to Depends(get_session). @@ -51,10 +48,9 @@ async def get_editions(db: Session = Depends(get_session), user: User = Depends( return EditionList(editions=user.editions) -@editions_router.get("/{edition_name}", response_model=Edition, tags=[Tags.EDITIONS], - dependencies=[Depends(require_coach)]) +@editions_router.get("/{edition_name}", response_model=Edition, tags=[Tags.EDITIONS]) async def get_edition_by_name(edition_name: str, db: Session = Depends(get_session), - user: User = Depends(get_current_active_user)): + user: User = Depends(require_coach)): """Get a specific edition. Args: @@ -65,11 +61,7 @@ async def get_edition_by_name(edition_name: str, db: Session = Depends(get_sessi Returns: Edition: an edition. """ - edition = logic_editions.get_edition_by_name(db, edition_name) - if not user.admin and edition not in user.editions: - raise MissingPermissionsException - - return 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], diff --git a/backend/src/app/utils/edition_readonly.py b/backend/src/app/utils/edition_readonly.py index 3f1386ed3..849fbd43a 100644 --- a/backend/src/app/utils/edition_readonly.py +++ b/backend/src/app/utils/edition_readonly.py @@ -6,6 +6,7 @@ def check_readonly_edition(db: Session, edition: Edition): + """Checks if the given edition is the latest one (others are read-only)""" latest = latest_edition(db) if edition != latest: raise ReadOnlyEditionException diff --git a/backend/src/database/crud/editions.py b/backend/src/database/crud/editions.py index 00a810a44..054d9d205 100644 --- a/backend/src/database/crud/editions.py +++ b/backend/src/database/crud/editions.py @@ -1,5 +1,5 @@ from sqlalchemy.orm import Session -from sqlalchemy import exc +from sqlalchemy import exc, func from src.app.exceptions.editions import DuplicateInsertException from src.database.models import Edition from src.app.schemas.editions import EditionBase @@ -65,4 +65,5 @@ def delete_edition(db: Session, edition_name: str): def latest_edition(db: Session) -> Edition: """Returns the latest edition from the database""" - return db.query(Edition).all()[-1] + max_edition_id = db.query(func.max(Edition.edition_id)).scalar() + return db.query(Edition).where(Edition.edition_id == max_edition_id).one() From 1da4c2db95da0827b962682f2f3a0d7b59e0c553 Mon Sep 17 00:00:00 2001 From: beguille Date: Fri, 8 Apr 2022 16:09:05 +0200 Subject: [PATCH 280/536] changed tests to authclient --- backend/src/app/exceptions/handlers.py | 2 +- backend/src/app/routers/editions/editions.py | 6 +++--- .../routers/editions/projects/students/projects_students.py | 4 ++-- .../test_editions/test_projects/test_projects.py | 5 +++-- .../test_projects/test_students/test_students.py | 5 +++-- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/backend/src/app/exceptions/handlers.py b/backend/src/app/exceptions/handlers.py index 7b7c3245a..1571d5383 100644 --- a/backend/src/app/exceptions/handlers.py +++ b/backend/src/app/exceptions/handlers.py @@ -89,7 +89,7 @@ def student_in_conflict_exception(_request: Request, _exception: StudentInConfli ) @app.exception_handler(FailedToAddProjectRoleException) - def student_in_conflict_exception(_request: Request, _exception: FailedToAddProjectRoleException): + def failed_to_add_project_role_exception(_request: Request, _exception: FailedToAddProjectRoleException): return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, content={'message': 'Something went wrong while adding this student to the project'} diff --git a/backend/src/app/routers/editions/editions.py b/backend/src/app/routers/editions/editions.py index 0312d8761..57bc5952c 100644 --- a/backend/src/app/routers/editions/editions.py +++ b/backend/src/app/routers/editions/editions.py @@ -48,9 +48,9 @@ async def get_editions(db: Session = Depends(get_session), user: User = Depends( return EditionList(editions=user.editions) -@editions_router.get("/{edition_name}", response_model=Edition, tags=[Tags.EDITIONS]) -async def get_edition_by_name(edition_name: str, db: Session = Depends(get_session), - user: User = Depends(require_coach)): +@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: 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 4a0bf76bb..5e907b3f1 100644 --- a/backend/src/app/routers/editions/projects/students/projects_students.py +++ b/backend/src/app/routers/editions/projects/students/projects_students.py @@ -24,7 +24,7 @@ async def remove_student_from_project(student_id: int, db: Session = Depends(get logic_remove_student_project(db, project, student_id, edition) -@project_students_router.patch("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, dependencies=[Depends(require_coach)]) +@project_students_router.patch("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response) 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), edition: Edition = Depends(get_edition)): @@ -34,7 +34,7 @@ async def change_project_role(student_id: int, input_sr: InputStudentRole, db: S logic_change_project_role(db, project, student_id, input_sr.skill_id, user.user_id, edition) -@project_students_router.post("/{student_id}", status_code=status.HTTP_201_CREATED, response_class=Response, dependencies=[Depends(require_coach)]) +@project_students_router.post("/{student_id}", status_code=status.HTTP_201_CREATED, response_class=Response) 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), edition: Edition = Depends(get_edition)): 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 3fcb85f82..b877c45af 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 @@ -275,13 +275,14 @@ def test_patch_wrong_project(database_session: Session, auth_client: AuthClient) assert json['projects'][0]['name'] == 'project' -def test_create_project_old_edition(database_with_data: Session, test_client: TestClient): +def test_create_project_old_edition(database_with_data: Session, 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() response = \ - test_client.post("/editions/ed2022/projects/", + auth_client.post("/editions/ed2022/projects/", json={"name": "test", "number_of_students": 5, "skills": [1, 1, 1, 1, 1], "partners": ["ugent"], "coaches": [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 eaa38fd5f..737ebb907 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 @@ -304,12 +304,13 @@ def test_get_conflicts(database_with_data: Session, current_edition: Edition, au assert json['editionName'] == "ed2022" -def test_add_student_project_old_edition(database_with_data: Session, test_client: TestClient): +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 = test_client.post( + 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 From 76f62980518fa30d73ea4c66d2a3ab8cc273e4ac Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 8 Apr 2022 16:31:40 +0200 Subject: [PATCH 281/536] added changes from develop to this branch --- backend/migrations/versions/1862d7dea4cc_.py | 24 ++++++++ backend/src/app/schemas/suggestion.py | 1 - .../test_database/test_crud/test_students.py | 10 ++- .../test_crud/test_suggestions.py | 10 ++- .../test_suggestions/test_suggestions.py | 61 +++++++++---------- 5 files changed, 61 insertions(+), 45 deletions(-) create mode 100644 backend/migrations/versions/1862d7dea4cc_.py diff --git a/backend/migrations/versions/1862d7dea4cc_.py b/backend/migrations/versions/1862d7dea4cc_.py new file mode 100644 index 000000000..094e930eb --- /dev/null +++ b/backend/migrations/versions/1862d7dea4cc_.py @@ -0,0 +1,24 @@ +"""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/src/app/schemas/suggestion.py b/backend/src/app/schemas/suggestion.py index b8bed700c..0794193b3 100644 --- a/backend/src/app/schemas/suggestion.py +++ b/backend/src/app/schemas/suggestion.py @@ -13,7 +13,6 @@ class User(CamelCaseModel): #TODO: delete this when user is on develop and use t """ user_id: int name: str - email: str class Config: orm_mode = True diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index a1c1fb434..a612ab823 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -9,19 +9,17 @@ def database_with_data(database_session: Session): """A function to fill the database with fake data that can easly be used when testing""" # Editions - edition: Edition = Edition(year=2022) + edition: Edition = Edition(year=2022, name="ed22") database_session.add(edition) database_session.commit() # Users - admin: User = User(name="admin", email="admin@ngmail.com", admin=True) - coach1: User = User(name="coach1", email="coach1@noutlook.be") - coach2: User = User(name="coach2", email="coach2@noutlook.be") - request: User = User(name="request", email="request@ngmail.com") + admin: User = User(name="admin", admin=True) + coach1: User = User(name="coach1") + coach2: User = User(name="coach2") database_session.add(admin) database_session.add(coach1) database_session.add(coach2) - database_session.add(request) database_session.commit() # Skill diff --git a/backend/tests/test_database/test_crud/test_suggestions.py b/backend/tests/test_database/test_crud/test_suggestions.py index 5501d91ff..b179dd919 100644 --- a/backend/tests/test_database/test_crud/test_suggestions.py +++ b/backend/tests/test_database/test_crud/test_suggestions.py @@ -13,19 +13,17 @@ def database_with_data(database_session: Session): """A function to fill the database with fake data that can easly be used when testing""" # Editions - edition: Edition = Edition(year=2022) + edition: Edition = Edition(year=2022, name="ed22") database_session.add(edition) database_session.commit() # Users - admin: User = User(name="admin", email="admin@ngmail.com", admin=True) - coach1: User = User(name="coach1", email="coach1@noutlook.be") - coach2: User = User(name="coach2", email="coach2@noutlook.be") - request: User = User(name="request", email="request@ngmail.com") + admin: User = User(name="admin", admin=True) + coach1: User = User(name="coach1") + coach2: User = User(name="coach2") database_session.add(admin) database_session.add(coach1) database_session.add(coach2) - database_session.add(request) database_session.commit() # Skill 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 8c7d449c4..bcebd9dda 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 @@ -13,31 +13,30 @@ def database_with_data(database_session: Session) -> Session: """A fixture to fill the database with fake data that can easly be used when testing""" # Editions - edition: Edition = Edition(year=2022) + edition: Edition = Edition(year=2022, name="ed2022") database_session.add(edition) database_session.commit() # Users - admin: User = User(name="admin", email="admin@ngmail.com", admin=True, editions=[edition]) - coach1: User = User(name="coach1", email="coach1@noutlook.be", editions=[edition]) - coach2: User = User(name="coach2", email="coach2@noutlook.be", editions=[edition]) - request: User = User(name="request", email="request@ngmail.com", editions=[edition]) + admin: User = User(name="admin", admin=True, editions=[edition]) + coach1: User = User(name="coach1", editions=[edition]) + coach2: User = User(name="coach2", editions=[edition]) database_session.add(admin) database_session.add(coach1) database_session.add(coach2) - database_session.add(request) database_session.commit() # AuthEmail pw_hash = get_password_hash("wachtwoord") - auth_email_admin: AuthEmail = AuthEmail(user=admin, pw_hash=pw_hash) - auth_email_coach1: AuthEmail = AuthEmail(user=coach1, pw_hash=pw_hash) - auth_email_coach2: AuthEmail = AuthEmail(user=coach2, pw_hash=pw_hash) - auth_email_request: AuthEmail = AuthEmail(user=request, pw_hash=pw_hash) + auth_email_admin: AuthEmail = AuthEmail( + user=admin, email="admin@ngmail.com", pw_hash=pw_hash) + auth_email_coach1: AuthEmail = AuthEmail( + user=coach1, email="coach1@noutlook.be", pw_hash=pw_hash) + auth_email_coach2: AuthEmail = AuthEmail( + user=coach2, email="coach2@noutlook.be", pw_hash=pw_hash) database_session.add(auth_email_admin) database_session.add(auth_email_coach1) database_session.add(auth_email_coach2) - database_session.add(auth_email_request) database_session.commit() # Skill @@ -87,6 +86,7 @@ def auth_coach1(test_client: TestClient) -> str: auth = "Bearer " + token return auth + @pytest.fixture def auth_coach2(test_client: TestClient) -> str: """A fixture for logging in coach1""" @@ -99,6 +99,7 @@ def auth_coach2(test_client: TestClient) -> str: auth = "Bearer " + token return auth + @pytest.fixture def auth_admin(test_client: TestClient) -> str: """A fixture for logging in admin""" @@ -115,14 +116,12 @@ def auth_admin(test_client: TestClient) -> str: def test_new_suggestion(database_with_data: Session, test_client: TestClient, auth_coach1): """Tests a new sugesstion""" - resp = test_client.post("/editions/1/students/2/suggestions/", headers={ + resp = test_client.post("/editions/ed2022/students/2/suggestions/", headers={ "Authorization": auth_coach1}, 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() assert len(suggestions) == 1 - assert resp.json()[ - "suggestion"]["coach"]["email"] == suggestions[0].coach.email assert DecisionEnum(resp.json()["suggestion"] ["suggestion"]) == suggestions[0].suggestion assert resp.json()[ @@ -132,7 +131,7 @@ def test_new_suggestion(database_with_data: Session, test_client: TestClient, au def test_new_suggestion_not_authorized(database_with_data: Session, test_client: TestClient): """Tests when not authorized you can't add a new suggestion""" - assert test_client.post("/editions/1/students/2/suggestions/", headers={"Authorization": "auth"}, json={ + assert test_client.post("/editions/ed2022/students/2/suggestions/", headers={"Authorization": "auth"}, 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() @@ -142,7 +141,7 @@ def test_new_suggestion_not_authorized(database_with_data: Session, test_client: def test_get_suggestions_of_student_not_authorized(database_with_data: Session, test_client: TestClient): """Tests if you don't have the right access, you get the right HTTP code""" - assert test_client.get("/editions/1/students/29/suggestions/", headers={"Authorization": "auth"}, json={ + assert test_client.get("/editions/ed2022/students/29/suggestions/", headers={"Authorization": "auth"}, json={ "suggestion": 1, "argumentation": "Ja"}).status_code == status.HTTP_401_UNAUTHORIZED @@ -150,47 +149,45 @@ def test_get_suggestions_of_ghost(database_with_data: Session, test_client: Test """Tests if the student don't exist, you get a 404""" res = test_client.get( - "/editions/1/students/9000/suggestions/", headers={"Authorization": auth_coach1}) + "/editions/ed2022/students/9000/suggestions/", headers={"Authorization": auth_coach1}) assert res.status_code == status.HTTP_404_NOT_FOUND def test_get_suggestions_of_student(database_with_data: Session, test_client: TestClient, auth_coach1: str, auth_admin: str): """Tests to get the suggestions of a student""" - assert test_client.post("/editions/1/students/2/suggestions/", headers={"Authorization": auth_coach1}, json={ + assert test_client.post("/editions/ed2022/students/2/suggestions/", headers={"Authorization": auth_coach1}, json={ "suggestion": 1, "argumentation": "Ja"}).status_code == status.HTTP_201_CREATED - assert test_client.post("/editions/1/students/2/suggestions/", headers={"Authorization": auth_admin}, json={ + assert test_client.post("/editions/ed2022/students/2/suggestions/", headers={"Authorization": auth_admin}, json={ "suggestion": 3, "argumentation": "Neen"}).status_code == status.HTTP_201_CREATED res = test_client.get( "/editions/1/students/2/suggestions/", headers={"Authorization": auth_admin}) assert res.status_code == status.HTTP_200_OK res_json = res.json() assert len(res_json["suggestions"]) == 2 - assert res_json["suggestions"][0]["coach"]["email"] == "coach1@noutlook.be" assert res_json["suggestions"][0]["suggestion"] == 1 assert res_json["suggestions"][0]["argumentation"] == "Ja" - assert res_json["suggestions"][1]["coach"]["email"] == "admin@ngmail.com" assert res_json["suggestions"][1]["suggestion"] == 3 assert res_json["suggestions"][1]["argumentation"] == "Neen" def test_delete_ghost_suggestion(database_with_data: Session, test_client: TestClient, auth_coach1: str): """Tests that you get the correct status code when you delete a not existing suggestion""" - assert test_client.delete("/editions/1/students/1/suggestions/8000", headers={ + assert test_client.delete("/editions/ed2022/students/1/suggestions/8000", headers={ "Authorization": auth_coach1}).status_code == status.HTTP_404_NOT_FOUND def test_delete_not_autorized(database_with_data: Session, test_client: TestClient): """Tests that you have to be loged in for deleating a suggestion""" - assert test_client.delete("/editions/1/students/1/suggestions/8000", headers={ + assert test_client.delete("/editions/ed2022/students/1/suggestions/8000", headers={ "Authorization": "auth"}).status_code == status.HTTP_401_UNAUTHORIZED def test_delete_suggestion_admin(database_with_data: Session, test_client: TestClient, auth_admin: str): """Test that an admin can update suggestions""" - assert test_client.delete("/editions/1/students/1/suggestions/1", headers={ + assert test_client.delete("/editions/ed2022/students/1/suggestions/1", headers={ "Authorization": auth_admin}).status_code == status.HTTP_204_NO_CONTENT suggestions: Suggestion = database_with_data.query( Suggestion).where(Suggestion.suggestion_id == 1).all() @@ -200,7 +197,7 @@ def test_delete_suggestion_admin(database_with_data: Session, test_client: TestC def test_delete_suggestion_coach_their_review(database_with_data: Session, test_client: TestClient, auth_coach1: str): """Tests that a coach can delete their own suggestion""" - assert test_client.delete("/editions/1/students/1/suggestions/1", headers={ + assert test_client.delete("/editions/ed2022/students/1/suggestions/1", headers={ "Authorization": auth_coach1}).status_code == status.HTTP_204_NO_CONTENT suggestions: Suggestion = database_with_data.query( Suggestion).where(Suggestion.suggestion_id == 1).all() @@ -210,7 +207,7 @@ def test_delete_suggestion_coach_their_review(database_with_data: Session, test_ def test_delete_suggestion_coach_other_review(database_with_data: Session, test_client: TestClient, auth_coach2: str): """Tests that a coach can't delete other coaches their suggestions""" - assert test_client.delete("/editions/1/students/1/suggestions/1", headers={ + assert test_client.delete("/editions/ed2022/students/1/suggestions/1", headers={ "Authorization": auth_coach2}).status_code == status.HTTP_403_FORBIDDEN suggestions: Suggestion = database_with_data.query( Suggestion).where(Suggestion.suggestion_id == 1).all() @@ -219,21 +216,21 @@ def test_delete_suggestion_coach_other_review(database_with_data: Session, test_ def test_update_ghost_suggestion(database_with_data: Session, test_client: TestClient, auth_admin: str): """Tests a suggestion that don't exist """ - - assert test_client.put("/editions/1/students/1/suggestions/8000", headers={"Authorization": auth_admin}, json={ + + assert test_client.put("/editions/ed2022/students/1/suggestions/8000", headers={"Authorization": auth_admin}, json={ "suggestion": 1, "argumentation": "test"}).status_code == status.HTTP_404_NOT_FOUND def test_update_not_autorized(database_with_data: Session, test_client: TestClient): """Tests update when not autorized""" - assert test_client.put("/editions/1/students/1/suggestions/8000", headers={"Authorization": "auth"}, json={ + assert test_client.put("/editions/ed2022/students/1/suggestions/8000", headers={"Authorization": "auth"}, json={ "suggestion": 1, "argumentation": "test"}).status_code == status.HTTP_401_UNAUTHORIZED def test_update_suggestion_admin(database_with_data: Session, test_client: TestClient, auth_admin: str): """Test that an admin can update suggestions""" - assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth_admin}, json={ + assert test_client.put("/editions/ed2022/students/1/suggestions/1", headers={"Authorization": auth_admin}, 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() @@ -244,7 +241,7 @@ def test_update_suggestion_admin(database_with_data: Session, test_client: TestC def test_update_suggestion_coach_their_review(database_with_data: Session, test_client: TestClient, auth_coach1: str): """Tests that a coach can update their own suggestion""" - assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth_coach1}, json={ + assert test_client.put("/editions/ed2022/students/1/suggestions/1", headers={"Authorization": auth_coach1}, 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() @@ -255,7 +252,7 @@ def test_update_suggestion_coach_their_review(database_with_data: Session, test_ def test_update_suggestion_coach_other_review(database_with_data: Session, test_client: TestClient, auth_coach2: str): """Tests that a coach can't update other coaches their suggestions""" - assert test_client.put("/editions/1/students/1/suggestions/1", headers={"Authorization": auth_coach2}, json={ + assert test_client.put("/editions/ed2022/students/1/suggestions/1", headers={"Authorization": auth_coach2}, json={ "suggestion": 3, "argumentation": "test"}).status_code == status.HTTP_403_FORBIDDEN suggestion: Suggestion = database_with_data.query( Suggestion).where(Suggestion.suggestion_id == 1).one() From d0ef0af81f0bf0a3d186563a6b2ceffaa5091366 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 8 Apr 2022 17:00:43 +0200 Subject: [PATCH 282/536] Fix param casing --- frontend/src/Router.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 53ce61f27..2fcc3c433 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -51,7 +51,7 @@ export default function Router() { {/* TODO create edition page */} } /> - }> + }> {/* TODO edition page? do we need? maybe just some nav/links? */} } /> From a14923ce5db14c92298cb6b6a924726e06ba9e7b Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 17:09:28 +0200 Subject: [PATCH 283/536] add tests --- .../test_database/test_crud/test_projects.py | 2 +- .../test_editions/test_editions.py | 17 +++++ .../test_invites/test_invites.py | 19 +++++ .../test_projects/test_projects.py | 20 +++++ .../test_routers/test_users/test_users.py | 75 ++++++++++++++++++- 5 files changed, 131 insertions(+), 2 deletions(-) diff --git a/backend/tests/test_database/test_crud/test_projects.py b/backend/tests/test_database/test_crud/test_projects.py index dee67b51e..09f41bbfa 100644 --- a/backend/tests/test_database/test_crud/test_projects.py +++ b/backend/tests/test_database/test_crud/test_projects.py @@ -78,7 +78,7 @@ def test_get_all_projects(database_with_data: Session, current_edition: Edition) def test_get_all_projects_pagination(database_session: Session): - """test get all projects""" + """test get all projects paginated""" edition = Edition(year=2022, name="ed2022") database_session.add(edition) 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 260477e89..2094119d5 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,6 +1,7 @@ from sqlalchemy.orm import Session from starlette import status +from settings import DB_PAGE_SIZE from src.database.models import Edition from tests.utils.authorization import AuthClient @@ -28,6 +29,22 @@ 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): + """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() + + 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 + + def test_get_edition_by_name_admin(database_session: Session, auth_client: AuthClient): """Test getting an edition as an admin""" auth_client.admin() 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 97f0af1e2..003816b94 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 @@ -4,6 +4,7 @@ from sqlalchemy.orm import Session from starlette import status +from settings import DB_PAGE_SIZE from src.database.models import Edition, InviteLink from tests.utils.authorization import AuthClient @@ -41,6 +42,24 @@ def test_get_invites(database_session: Session, auth_client: AuthClient): assert link["editionName"] == "ed2022" +def test_get_invites_paginated(database_session: Session, 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() + + 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 + + def test_create_invite_valid(database_session: Session, auth_client: AuthClient): """Test endpoint for creating invites when data is valid""" auth_client.admin() 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 f5f8978ac..7461d7745 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 @@ -2,6 +2,7 @@ from sqlalchemy.orm import Session from starlette import status +from settings import DB_PAGE_SIZE from src.database.models import Edition, Project, User, Skill, ProjectRole, Student, Partner from tests.utils.authorization import AuthClient @@ -63,6 +64,25 @@ def test_get_projects(database_with_data: Session, auth_client: AuthClient): assert json['projects'][2]['name'] == "project3" +def test_get_projects_paginated(database_session: Session, 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() + + auth_client.admin() + + 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): """Tests get a specific project""" auth_client.admin() diff --git a/backend/tests/test_routers/test_users/test_users.py b/backend/tests/test_routers/test_users/test_users.py index 04c1997d0..32b90d03f 100644 --- a/backend/tests/test_routers/test_users/test_users.py +++ b/backend/tests/test_routers/test_users/test_users.py @@ -5,6 +5,7 @@ from starlette import status +from settings import DB_PAGE_SIZE from src.database import models from src.database.models import user_editions, CoachRequest from tests.utils.authorization import AuthClient @@ -65,6 +66,22 @@ def test_get_all_users(database_session: Session, auth_client: AuthClient, data: assert data["user2"] in user_ids +def test_get_all_users_paginated(database_session: Session, 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() + + 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. + + def test_get_users_response(database_session: Session, auth_client: AuthClient, data: dict[str, str]): """Test the response model of a user""" auth_client.admin() @@ -99,6 +116,25 @@ def test_get_users_from_edition(database_session: Session, auth_client: AuthClie assert [data["user2"]] == user_ids +def test_get_all_users_for_edition_paginated(database_session: Session, 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() + + 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. + + def test_get_admins_from_edition(database_session: Session, auth_client: AuthClient, data: dict[str, str | int]): """Test endpoint for getting a list of admins from a given edition""" auth_client.admin() @@ -187,7 +223,6 @@ def test_remove_coach(database_session: Session, auth_client: AuthClient): assert len(coach) == 0 - def test_remove_coach_all_editions(database_session: Session, auth_client: AuthClient): """Test removing a user as coach from all editions""" auth_client.admin() @@ -256,6 +291,25 @@ def test_get_all_requests(database_session: Session, auth_client: AuthClient): assert user2.user_id in user_ids +def test_get_all_requests_paginated(database_session: Session, auth_client: AuthClient): + """Test endpoint for getting a paginated list of requests""" + edition = models.Edition(year=2022, name="ed2022") + + for i in range(round(DB_PAGE_SIZE * 1.5)): + 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() + + 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 + + def test_get_all_requests_from_edition(database_session: Session, auth_client: AuthClient): """Test endpoint for getting all userrequests of a given edition""" auth_client.admin() @@ -295,6 +349,25 @@ def test_get_all_requests_from_edition(database_session: Session, auth_client: A assert user2.user_id == requests[0]["user"]["userId"] +def test_get_all_requests_for_edition_paginated(database_session: Session, auth_client: AuthClient): + """Test endpoint for getting a paginated list of requests""" + edition = models.Edition(year=2022, name="ed2022") + + for i in range(round(DB_PAGE_SIZE * 1.5)): + 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() + + 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 + + def test_accept_request(database_session, auth_client: AuthClient): """Test endpoint for accepting a coach request""" auth_client.admin() From 0dfa41a2376b6004eddaf85c8cde1f558966ce40 Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 17:15:30 +0200 Subject: [PATCH 284/536] also paginate ?admin=true --- backend/src/app/logic/users.py | 4 ++-- backend/src/database/crud/users.py | 21 +++++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index 93aaaf499..10a5b8879 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -12,9 +12,9 @@ def get_users_list(db: Session, admin: bool, edition_name: str | None, page: int """ if admin: if edition_name is None: - users_orm = users_crud.get_admins(db) + users_orm = users_crud.get_admins_page(db, page) else: - users_orm = users_crud.get_admins_for_edition(db, edition_name) + users_orm = users_crud.get_admins_for_edition_page(db, edition_name, page) else: if edition_name is None: users_orm = users_crud.get_users_page(db, page) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 05202739a..4d660f885 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -5,17 +5,26 @@ from src.database.models import user_editions, User, Edition, CoachRequest, AuthGoogle, AuthEmail, AuthGitHub +def _get_admins_query(db: Session) -> Query: + return db.query(User) \ + .where(User.admin) \ + .join(AuthEmail, isouter=True) \ + .join(AuthGitHub, isouter=True) \ + .join(AuthGoogle, isouter=True) + + def get_admins(db: Session) -> list[User]: """ Get all admins """ + return _get_admins_query(db).all() - return db.query(User) \ - .where(User.admin) \ - .join(AuthEmail, isouter=True) \ - .join(AuthGitHub, isouter=True) \ - .join(AuthGoogle, isouter=True) \ - .all() + +def get_admins_page(db: Session, page: int) -> list[User]: + """ + Get all admins paginated + """ + return paginate(_get_admins_query(db), page).all() def _get_users_query(db: Session) -> Query: From e32b7bb1a38dd8c200557f3de0012515d68e3519 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 8 Apr 2022 17:22:25 +0200 Subject: [PATCH 285/536] use de user scheme of users --- backend/src/app/logic/suggestions.py | 24 ++++++++++++++++++------ backend/src/app/schemas/suggestion.py | 26 +++++++++++++++----------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py index da4b7043e..3b2d183e0 100644 --- a/backend/src/app/logic/suggestions.py +++ b/backend/src/app/logic/suggestions.py @@ -1,20 +1,29 @@ +from setuptools import SetuptoolsDeprecationWarning from sqlalchemy.orm import Session from src.app.schemas.suggestion import NewSuggestion from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student, delete_suggestion, update_suggestion from src.database.models import Suggestion, User -from src.app.schemas.suggestion import SuggestionListResponse, SuggestionResponse +from src.app.schemas.suggestion import SuggestionListResponse, SuggestionResponse, suggestion_model_to_schema from src.app.exceptions.authentication import MissingPermissionsException + def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, user: User, student_id: int) -> SuggestionResponse: """"Make a new suggestion""" - suggestion_orm = create_suggestion(db, user.user_id, student_id, new_suggestion.suggestion, new_suggestion.argumentation) - return SuggestionResponse(suggestion=suggestion_orm) + suggestion_orm = create_suggestion( + db, user.user_id, student_id, new_suggestion.suggestion, new_suggestion.argumentation) + suggestion = suggestion_model_to_schema(suggestion_orm) + return SuggestionResponse(suggestion=suggestion) + def all_suggestions_of_student(db: Session, student_id: int) -> SuggestionListResponse: """Get all suggestions of a student""" suggestions_orm = get_suggestions_of_student(db, student_id) - return SuggestionListResponse(suggestions=suggestions_orm) + 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: """ @@ -28,14 +37,17 @@ def remove_suggestion(db: Session, suggestion: Suggestion, user: User) -> None: else: raise MissingPermissionsException + def change_suggestion(db: Session, new_suggestion: NewSuggestion, suggestion: Suggestion, user: User) -> None: """ Update a suggestion Admins can update all suggestions, coaches only their own suggestions """ if user.admin: - update_suggestion(db,suggestion,new_suggestion.suggestion, new_suggestion.argumentation) + update_suggestion( + db, suggestion, new_suggestion.suggestion, new_suggestion.argumentation) elif suggestion.coach == user: - update_suggestion(db,suggestion,new_suggestion.suggestion, new_suggestion.argumentation) + update_suggestion( + db, suggestion, new_suggestion.suggestion, new_suggestion.argumentation) else: raise MissingPermissionsException diff --git a/backend/src/app/schemas/suggestion.py b/backend/src/app/schemas/suggestion.py index 0794193b3..04745b6d0 100644 --- a/backend/src/app/schemas/suggestion.py +++ b/backend/src/app/schemas/suggestion.py @@ -1,21 +1,14 @@ from src.app.schemas.webhooks import CamelCaseModel from src.database.enums import DecisionEnum +from src.app.schemas.users import User, user_model_to_schema +from src.database.models import Suggestion as Suggestion_model + class NewSuggestion(CamelCaseModel): """The fields of a suggestion""" suggestion: DecisionEnum argumentation: str -class User(CamelCaseModel): #TODO: delete this when user is on develop and use that one - """ - Model to represent a Coach - Sent as a response to API /GET requests - """ - user_id: int - name: str - - class Config: - orm_mode = True class Suggestion(CamelCaseModel): """ @@ -24,21 +17,32 @@ class Suggestion(CamelCaseModel): """ suggestion_id: int - coach : User + coach: User suggestion: DecisionEnum argumentation: str class Config: orm_mode = True + class SuggestionListResponse(CamelCaseModel): """ A list of suggestions models """ suggestions: list[Suggestion] + class SuggestionResponse(CamelCaseModel): """ the suggestion that is created """ suggestion: Suggestion + + +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) + return Suggestion(suggestion_id=suggestion_model.suggestion_id, + coach=coach, + suggestion=suggestion_model.suggestion, + argumentation=suggestion_model.argumentation) From 52dd0968591bedd24e5a605e5ef1a3432510375f Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 8 Apr 2022 17:27:15 +0200 Subject: [PATCH 286/536] Use edition names in frontend --- frontend/src/contexts/auth-context.tsx | 8 ++++---- frontend/src/utils/api/login.ts | 3 ++- .../src/views/VerifyingTokenPage/VerifyingTokenPage.tsx | 2 ++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx index bdab830e2..84b8c3fc2 100644 --- a/frontend/src/contexts/auth-context.tsx +++ b/frontend/src/contexts/auth-context.tsx @@ -13,8 +13,8 @@ export interface AuthContextState { setRole: (value: Role | null) => void; token: string | null; setToken: (value: string | null) => void; - editions: number[]; - setEditions: (value: number[]) => void; + editions: string[]; + setEditions: (value: string[]) => void; } /** @@ -31,7 +31,7 @@ function authDefaultState(): AuthContextState { token: getToken(), setToken: (_: string | null) => {}, editions: [], - setEditions: (_: number[]) => {}, + setEditions: (_: string[]) => {}, }; } @@ -54,7 +54,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([]); // Default value: check LocalStorage const [token, setToken] = useState(getToken()); diff --git a/frontend/src/utils/api/login.ts b/frontend/src/utils/api/login.ts index bc2ae5570..61762c64b 100644 --- a/frontend/src/utils/api/login.ts +++ b/frontend/src/utils/api/login.ts @@ -7,7 +7,7 @@ interface LoginResponse { accessToken: string; user: { admin: boolean; - editions: number[]; + editions: string[]; }; } @@ -29,6 +29,7 @@ export async function logIn(auth: AuthContextState, email: string, password: str auth.setToken(login.accessToken); auth.setIsLoggedIn(true); auth.setRole(login.user.admin ? Role.ADMIN : Role.COACH); + auth.setEditions(login.user.editions); return true; } catch (error) { diff --git a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx index 82b2dbbdf..cdbd7f116 100644 --- a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx +++ b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx @@ -20,11 +20,13 @@ export default function VerifyingTokenPage() { authContext.setToken(null); authContext.setIsLoggedIn(false); authContext.setRole(null); + authContext.setEditions([]); } else { // Token was valid, use it as the default request header setBearerToken(authContext.token); authContext.setIsLoggedIn(true); authContext.setRole(response.admin ? Role.ADMIN : Role.COACH); + authContext.setEditions(response.editions); } }; From 2dd1c0c74eadf5bff5b187cb218a2cc5d3958802 Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 17:37:51 +0200 Subject: [PATCH 287/536] ignore ?edition= when listing admins --- backend/src/app/logic/users.py | 5 +--- backend/src/app/routers/users/users.py | 3 +++ backend/src/database/crud/users.py | 21 ----------------- .../test_database/test_crud/test_users.py | 23 +++++++++---------- .../test_routers/test_users/test_users.py | 22 ++++++++++++++---- 5 files changed, 33 insertions(+), 41 deletions(-) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index 10a5b8879..dc3c94184 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -11,10 +11,7 @@ def get_users_list(db: Session, admin: bool, edition_name: str | None, page: int and wrap the result in a pydantic model """ if admin: - if edition_name is None: - users_orm = users_crud.get_admins_page(db, page) - else: - users_orm = users_crud.get_admins_for_edition_page(db, edition_name, page) + users_orm = users_crud.get_admins_page(db, page) else: if edition_name is None: users_orm = users_crud.get_users_page(db, page) diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index 9d1a8d19d..65f6a4ed4 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -16,6 +16,9 @@ async def get_users(admin: bool = Query(False), edition: str | None = Query(None db: Session = Depends(get_session)): """ Get users + + When the admin parameter is True, the edition parameter will have no effect. + Since admins have access to all editions. """ return logic.get_users_list(db, admin, edition, page) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 4d660f885..7aea83b7e 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -72,27 +72,6 @@ def get_users_for_edition_page(db: Session, edition_name: str, page: int) -> lis return paginate(_get_users_for_edition_query(db, get_edition_by_name(db, edition_name)), page).all() -def _get_admins_for_edition_query(db: Session, edition: Edition) -> Query: - return db.query(User) \ - .where(User.admin) \ - .join(user_editions) \ - .filter(user_editions.c.edition_id == edition.edition_id) - - -def get_admins_for_edition(db: Session, edition_name: str) -> list[User]: - """ - Get all admins from the given edition - """ - return _get_admins_for_edition_query(db, get_edition_by_name(db, edition_name)).all() - - -def get_admins_for_edition_page(db: Session, edition_name: str, page: int) -> list[User]: - """ - Get all admins from the given edition - """ - return paginate(_get_admins_for_edition_query(db, get_edition_by_name(db, edition_name)), page).all() - - def edit_admin_status(db: Session, user_id: int, admin: bool): """ Edit the admin-status of a user diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index 1c09e27b6..0cde4e3c4 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -77,6 +77,17 @@ def test_get_all_admins(database_session: Session, data: dict[str, str]): assert data["user1"] == users[0].user_id +def test_get_all_admins_paginated(database_session: Session): + for i in range(round(DB_PAGE_SIZE * 1.5)): + database_session.add(models.User(name=f"Project {i}", admin=True)) + database_session.commit() + + assert len(users_crud.get_admins_page(database_session, 0)) == DB_PAGE_SIZE + assert len(users_crud.get_admins_page(database_session, 1)) == round( + DB_PAGE_SIZE * 1.5 + ) - DB_PAGE_SIZE + + def test_get_user_edition_names_empty(database_session: Session): """Test getting all editions from a user when there are none""" user = models.User(name="test") @@ -154,18 +165,6 @@ def test_get_all_users_for_edition_paginated(database_session: Session): ) - DB_PAGE_SIZE -def test_get_admins_from_edition(database_session: Session, data: dict[str, str]): - """Test get request for admins of a given edition""" - - # get all admins from edition - users = users_crud.get_admins_for_edition(database_session, data["edition1"]) - assert len(users) == 1, "Wrong length" - assert data["user1"] == users[0].user_id - - users = users_crud.get_admins_for_edition(database_session, data["edition2"]) - assert len(users) == 0, "Wrong length" - - def test_edit_admin_status(database_session: Session): """Test changing the admin status of a user""" diff --git a/backend/tests/test_routers/test_users/test_users.py b/backend/tests/test_routers/test_users/test_users.py index 32b90d03f..9690b83ee 100644 --- a/backend/tests/test_routers/test_users/test_users.py +++ b/backend/tests/test_routers/test_users/test_users.py @@ -106,6 +106,21 @@ def test_get_all_admins(database_session: Session, auth_client: AuthClient, data assert [data["user1"]] == user_ids +def test_get_all_admins_paginated(database_session: Session, auth_client: AuthClient): + """Test endpoint for getting a list of paginated admins""" + for i in range(round(DB_PAGE_SIZE * 1.5)): + database_session.add(models.User(name=f"User {i}", admin=True)) + 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 + + def test_get_users_from_edition(database_session: Session, auth_client: AuthClient, data: dict[str, str | int]): """Test endpoint for getting a list of users from a given edition""" auth_client.admin() @@ -136,17 +151,16 @@ def test_get_all_users_for_edition_paginated(database_session: Session, auth_cli def test_get_admins_from_edition(database_session: Session, auth_client: AuthClient, data: dict[str, str | int]): - """Test endpoint for getting a list of admins from a given edition""" + """Test endpoint for getting a list of admins, edition should be ignored""" 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 - user_ids = [user["userId"] for user in response.json()['users']] - assert [data["user1"]] == user_ids + 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']) == 0 + assert len(response.json()['users']) == 2 def test_get_users_invalid(database_session: Session, auth_client: AuthClient, data: dict[str, str | int]): From 24cbad1509537f8439809f8f4622e8ae27b3ab82 Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 17:50:15 +0200 Subject: [PATCH 288/536] fix warnings --- backend/src/app/exceptions/handlers.py | 2 +- backend/src/database/crud/projects_students.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/app/exceptions/handlers.py b/backend/src/app/exceptions/handlers.py index 1f7c10327..f9ba95120 100644 --- a/backend/src/app/exceptions/handlers.py +++ b/backend/src/app/exceptions/handlers.py @@ -89,7 +89,7 @@ def student_in_conflict_exception(_request: Request, _exception: StudentInConfli ) @app.exception_handler(FailedToAddProjectRoleException) - def student_in_conflict_exception(_request: Request, _exception: FailedToAddProjectRoleException): + def failed_to_add_project_role_exception(_request: Request, _exception: FailedToAddProjectRoleException): return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, content={'message': 'Something went wrong while adding this student to the project'} diff --git a/backend/src/database/crud/projects_students.py b/backend/src/database/crud/projects_students.py index 49489de04..ac88b4b7e 100644 --- a/backend/src/database/crud/projects_students.py +++ b/backend/src/database/crud/projects_students.py @@ -41,6 +41,7 @@ def db_change_project_role(db: Session, project: Project, student_id: int, skill def db_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() From f7ebb2a971f5067f464cd33ffedb45e0e229a808 Mon Sep 17 00:00:00 2001 From: Francis Date: Fri, 8 Apr 2022 17:53:25 +0200 Subject: [PATCH 289/536] fix warnings --- backend/src/database/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/database/models.py b/backend/src/database/models.py index 1890b2a76..802cd5fa1 100644 --- a/backend/src/database/models.py +++ b/backend/src/database/models.py @@ -292,7 +292,7 @@ class User(Base): """Users of the tool (only admins & coaches)""" __tablename__ = "users" - user_id = Column(Integer, primary_key=True) + user_id: int = Column(Integer, primary_key=True) name = Column(Text, nullable=False) admin = Column(Boolean, nullable=False, default=False) From 23c08fd9a86ffcebfc3bf4d792354a93fe749077 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 8 Apr 2022 17:56:06 +0200 Subject: [PATCH 290/536] uses authclient --- .../test_suggestions/test_suggestions.py | 164 ++++++++---------- 1 file changed, 71 insertions(+), 93 deletions(-) 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 bcebd9dda..e305176cb 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,12 +1,12 @@ -from re import A import pytest from sqlalchemy.orm import Session from starlette import status -from starlette.testclient import TestClient from src.database.enums import DecisionEnum from src.database.models import Suggestion, Student, User, Edition, Skill, AuthEmail from src.app.logic.security import get_password_hash +from tests.utils.authorization import AuthClient + @pytest.fixture def database_with_data(database_session: Session) -> Session: @@ -74,50 +74,12 @@ def database_with_data(database_session: Session) -> Session: return database_session -@pytest.fixture -def auth_coach1(test_client: TestClient) -> str: - """A fixture for logging in coach1""" - - form = { - "username": "coach1@noutlook.be", - "password": "wachtwoord" - } - token = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token - return auth - - -@pytest.fixture -def auth_coach2(test_client: TestClient) -> str: - """A fixture for logging in coach1""" - - form = { - "username": "coach2@noutlook.be", - "password": "wachtwoord" - } - token = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token - return auth - - -@pytest.fixture -def auth_admin(test_client: TestClient) -> str: - """A fixture for logging in admin""" - - form = { - "username": "admin@ngmail.com", - "password": "wachtwoord" - } - token = test_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token - return auth - - -def test_new_suggestion(database_with_data: Session, test_client: TestClient, auth_coach1): +def test_new_suggestion(database_with_data: Session, auth_client: AuthClient): """Tests a new sugesstion""" - - resp = test_client.post("/editions/ed2022/students/2/suggestions/", headers={ - "Authorization": auth_coach1}, json={"suggestion": 1, "argumentation": "test"}) + 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() @@ -128,41 +90,43 @@ def test_new_suggestion(database_with_data: Session, test_client: TestClient, au "suggestion"]["argumentation"] == suggestions[0].argumentation -def test_new_suggestion_not_authorized(database_with_data: Session, test_client: TestClient): +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 test_client.post("/editions/ed2022/students/2/suggestions/", headers={"Authorization": "auth"}, json={ + 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 -def test_get_suggestions_of_student_not_authorized(database_with_data: Session, test_client: TestClient): +def test_get_suggestions_of_student_not_authorized(database_with_data: Session, auth_client: AuthClient): """Tests if you don't have the right access, you get the right HTTP code""" - assert test_client.get("/editions/ed2022/students/29/suggestions/", headers={"Authorization": "auth"}, json={ + assert auth_client.get("/editions/ed2022/students/29/suggestions/", headers={"Authorization": "auth"}, json={ "suggestion": 1, "argumentation": "Ja"}).status_code == status.HTTP_401_UNAUTHORIZED -def test_get_suggestions_of_ghost(database_with_data: Session, test_client: TestClient, auth_coach1: str): +def test_get_suggestions_of_ghost(database_with_data: Session, auth_client: AuthClient): """Tests if the student don't exist, you get a 404""" - - res = test_client.get( + edition: Edition = database_with_data.query(Edition).all()[0] + auth_client.coach(edition) + res = auth_client.get( "/editions/ed2022/students/9000/suggestions/", headers={"Authorization": auth_coach1}) assert res.status_code == status.HTTP_404_NOT_FOUND -def test_get_suggestions_of_student(database_with_data: Session, test_client: TestClient, auth_coach1: str, auth_admin: str): +def test_get_suggestions_of_student(database_with_data: Session, auth_client: AuthClient): """Tests to get the suggestions of a student""" - - assert test_client.post("/editions/ed2022/students/2/suggestions/", headers={"Authorization": auth_coach1}, json={ + 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 - - assert test_client.post("/editions/ed2022/students/2/suggestions/", headers={"Authorization": auth_admin}, json={ + auth_client.admin() + assert auth_client.post("/editions/ed2022/students/2/suggestions/", headers={"Authorization": auth_admin}, json={ "suggestion": 3, "argumentation": "Neen"}).status_code == status.HTTP_201_CREATED - res = test_client.get( - "/editions/1/students/2/suggestions/", headers={"Authorization": auth_admin}) + 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 @@ -172,65 +136,73 @@ def test_get_suggestions_of_student(database_with_data: Session, test_client: Te assert res_json["suggestions"][1]["argumentation"] == "Neen" -def test_delete_ghost_suggestion(database_with_data: Session, test_client: TestClient, auth_coach1: str): +def test_delete_ghost_suggestion(database_with_data: Session, auth_client: AuthClient): """Tests that you get the correct status code when you delete a not existing suggestion""" - assert test_client.delete("/editions/ed2022/students/1/suggestions/8000", headers={ - "Authorization": auth_coach1}).status_code == status.HTTP_404_NOT_FOUND + 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 -def test_delete_not_autorized(database_with_data: Session, test_client: TestClient): +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 test_client.delete("/editions/ed2022/students/1/suggestions/8000", headers={ - "Authorization": "auth"}).status_code == status.HTTP_401_UNAUTHORIZED + assert auth_client.delete( + "/editions/ed2022/students/1/suggestions/8000").status_code == status.HTTP_401_UNAUTHORIZED -def test_delete_suggestion_admin(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_delete_suggestion_admin(database_with_data: Session, auth_client: AuthClient): """Test that an admin can update suggestions""" - - assert test_client.delete("/editions/ed2022/students/1/suggestions/1", headers={ - "Authorization": auth_admin}).status_code == status.HTTP_204_NO_CONTENT + 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 -def test_delete_suggestion_coach_their_review(database_with_data: Session, test_client: TestClient, auth_coach1: str): +def test_delete_suggestion_coach_their_review(database_with_data: Session, auth_client: AuthClient): """Tests that a coach can delete their own suggestion""" - - assert test_client.delete("/editions/ed2022/students/1/suggestions/1", headers={ - "Authorization": auth_coach1}).status_code == status.HTTP_204_NO_CONTENT + 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 == 1).all() + Suggestion).where(Suggestion.suggestion_id == suggestion_id).all() assert len(suggestions) == 0 -def test_delete_suggestion_coach_other_review(database_with_data: Session, test_client: TestClient, auth_coach2: str): +def test_delete_suggestion_coach_other_review(database_with_data: Session, auth_client: AuthClient, auth_coach2: str): """Tests that a coach can't delete other coaches their suggestions""" - - assert test_client.delete("/editions/ed2022/students/1/suggestions/1", headers={ - "Authorization": auth_coach2}).status_code == status.HTTP_403_FORBIDDEN + 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 -def test_update_ghost_suggestion(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_update_ghost_suggestion(database_with_data: Session, auth_client: AuthClient): """Tests a suggestion that don't exist """ - - assert test_client.put("/editions/ed2022/students/1/suggestions/8000", headers={"Authorization": auth_admin}, json={ + 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 -def test_update_not_autorized(database_with_data: Session, test_client: TestClient): +def test_update_not_autorized(database_with_data: Session, auth_client: AuthClient): """Tests update when not autorized""" - assert test_client.put("/editions/ed2022/students/1/suggestions/8000", headers={"Authorization": "auth"}, json={ + assert 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, test_client: TestClient, auth_admin: str): +def test_update_suggestion_admin(database_with_data: Session, auth_client: AuthClient): """Test that an admin can update suggestions""" - - assert test_client.put("/editions/ed2022/students/1/suggestions/1", headers={"Authorization": auth_admin}, json={ + 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() @@ -238,21 +210,27 @@ def test_update_suggestion_admin(database_with_data: Session, test_client: TestC assert suggestion.argumentation == "test" -def test_update_suggestion_coach_their_review(database_with_data: Session, test_client: TestClient, auth_coach1: str): +def test_update_suggestion_coach_their_review(database_with_data: Session, auth_client: AuthClient): """Tests that a coach can update their own suggestion""" - - assert test_client.put("/editions/ed2022/students/1/suggestions/1", headers={"Authorization": auth_coach1}, json={ + 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 == 1).one() + 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, test_client: TestClient, auth_coach2: str): +def test_update_suggestion_coach_other_review(database_with_data: Session, auth_client: AuthClient): """Tests that a coach can't update other coaches their suggestions""" - - assert test_client.put("/editions/ed2022/students/1/suggestions/1", headers={"Authorization": auth_coach2}, json={ + 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() From 8c624a1e55a5d3fc8511482e20b4f288d9f332ec Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 8 Apr 2022 17:57:43 +0200 Subject: [PATCH 291/536] uses authclient --- .../test_students/test_suggestions/test_suggestions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 e305176cb..1bc8c9696 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 @@ -112,7 +112,7 @@ def test_get_suggestions_of_ghost(database_with_data: Session, auth_client: Auth edition: Edition = database_with_data.query(Edition).all()[0] auth_client.coach(edition) res = auth_client.get( - "/editions/ed2022/students/9000/suggestions/", headers={"Authorization": auth_coach1}) + "/editions/ed2022/students/9000/suggestions/") assert res.status_code == status.HTTP_404_NOT_FOUND @@ -123,7 +123,7 @@ def test_get_suggestions_of_student(database_with_data: Session, auth_client: Au 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/", headers={"Authorization": auth_admin}, json={ + 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/") @@ -175,7 +175,7 @@ def test_delete_suggestion_coach_their_review(database_with_data: Session, auth_ assert len(suggestions) == 0 -def test_delete_suggestion_coach_other_review(database_with_data: Session, auth_client: AuthClient, auth_coach2: str): +def test_delete_suggestion_coach_other_review(database_with_data: Session, 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) From cd21c8f28274931a19566143b64673afdb6d689e Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 8 Apr 2022 18:08:58 +0200 Subject: [PATCH 292/536] Properly return a user's editions when logging in --- backend/src/app/logic/users.py | 4 +-- backend/src/app/routers/login/login.py | 5 +++- backend/src/app/routers/users/users.py | 10 +++++-- backend/src/app/schemas/login.py | 11 ++----- backend/src/app/schemas/users.py | 1 - backend/src/database/crud/users.py | 13 +++++---- .../test_database/test_crud/test_users.py | 29 ++++++++++++++----- 7 files changed, 46 insertions(+), 27 deletions(-) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index 35dd8d888..5d5066b55 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -28,9 +28,9 @@ def get_users_list(db: Session, admin: bool, edition_name: str | None) -> UsersL return UsersListResponse(users=users) -def get_user_editions(user: User) -> list[str]: +def get_user_editions(db: Session, user: User) -> list[str]: """Get all names of the editions this user is coach in""" - return users_crud.get_user_edition_names(user) + return users_crud.get_user_edition_names(db, user) def edit_admin_status(db: Session, user_id: int, admin: AdminPatch): diff --git a/backend/src/app/routers/login/login.py b/backend/src/app/routers/login/login.py index 5f4b7afc0..b8d27162c 100644 --- a/backend/src/app/routers/login/login.py +++ b/backend/src/app/routers/login/login.py @@ -12,6 +12,7 @@ from src.app.logic.security import authenticate_user, create_access_token from src.app.routers.tags import Tags from src.app.schemas.login import Token, UserData +from src.app.schemas.users import user_model_to_schema from src.database.database import get_session login_router = APIRouter(prefix="/login", tags=[Tags.LOGIN]) @@ -33,5 +34,7 @@ async def login_for_access_token(db: Session = Depends(get_session), data={"sub": str(user.user_id)}, expires_delta=access_token_expires ) - user_data = UserData(admin=user.admin, editions=get_user_editions(user)) + user_data = user_model_to_schema(user).__dict__ + user_data["editions"] = get_user_editions(db, user) + return {"access_token": access_token, "token_type": "bearer", "user": user_data} diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index edcde7166..324a405c3 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -3,7 +3,8 @@ from src.app.routers.tags import Tags import src.app.logic.users as logic -from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, User as UserSchema +from src.app.schemas.login import UserData +from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, user_model_to_schema from src.app.utils.dependencies import require_admin, get_current_active_user from src.database.database import get_session from src.database.models import User as UserDB @@ -20,9 +21,12 @@ async def get_users(admin: bool = Query(False), edition: str | None = Query(None return logic.get_users_list(db, admin, edition) -@users_router.get("/current", response_model=UserSchema) -async def get_current_user(user: UserDB = Depends(get_current_active_user)): +@users_router.get("/current", response_model=UserData) +async def get_current_user(db: Session = Depends(get_session), user: UserDB = Depends(get_current_active_user)): """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) + return user diff --git a/backend/src/app/schemas/login.py b/backend/src/app/schemas/login.py index 02a6514ca..2ae9ed152 100644 --- a/backend/src/app/schemas/login.py +++ b/backend/src/app/schemas/login.py @@ -1,16 +1,11 @@ +from src.app.schemas.users import User from src.app.schemas.utils import CamelCaseModel -class UserData(CamelCaseModel): - """User information that can be passed to frontend - Includes the names of the editions a user is coach in - """ - admin: bool +class UserData(User): + """User information that can be passed to frontend""" editions: list[str] = [] - class Config: - orm_mode = True - class Token(CamelCaseModel): """Token generated after login diff --git a/backend/src/app/schemas/users.py b/backend/src/app/schemas/users.py index cbd55455c..9acdf3104 100644 --- a/backend/src/app/schemas/users.py +++ b/backend/src/app/schemas/users.py @@ -10,7 +10,6 @@ class Authentication(CamelCaseModel): class User(CamelCaseModel): """Model for a user""" - user_id: int name: str admin: bool diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index ceb869bcd..c826a5cbd 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -1,5 +1,6 @@ from sqlalchemy.orm import Session from src.database.models import user_editions, User, Edition, CoachRequest, AuthGoogle, AuthEmail, AuthGitHub +from src.database.crud.editions import get_editions def get_all_admins(db: Session) -> list[User]: @@ -23,14 +24,16 @@ def get_all_users(db: Session) -> list[User]: return db.query(User).all() -def get_user_edition_names(user: User) -> list[str]: - """Get all names of the editions this user is coach in""" +def get_user_edition_names(db: Session, user: User) -> list[str]: + """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) + + editions = [] # Name is 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 - - editions = [] - for edition in user.editions: + for edition in source: if edition.name is not None: editions.append(edition.name) diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index 660af7479..50941047a 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -72,29 +72,44 @@ def test_get_user_edition_names_empty(database_session: Session): database_session.commit() # No editions yet - editions = users_crud.get_user_edition_names(user) + editions = users_crud.get_user_edition_names(database_session, user) assert len(editions) == 0 -def test_get_user_edition_names(database_session: Session): - """Test getting all editions from a user when they aren't empty""" +def test_get_user_edition_names_admin(database_session: Session): + """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() + + # Not added to edition yet, but admin can see it anyway + editions = users_crud.get_user_edition_names(database_session, user) + assert len(editions) == 1 + + +def test_get_user_edition_names_coach(database_session: Session): + """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() # No editions yet - editions = users_crud.get_user_edition_names(user) + editions = users_crud.get_user_edition_names(database_session, user) assert len(editions) == 0 # Add user to a new edition - edition = models.Edition(year=2022, name="ed2022") user.editions.append(edition) - database_session.add(edition) database_session.add(user) database_session.commit() # No editions yet - editions = users_crud.get_user_edition_names(user) + editions = users_crud.get_user_edition_names(database_session, user) assert editions == [edition.name] From ed1cf194201e20464e9237599a85e42b684b3797 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 8 Apr 2022 18:13:41 +0200 Subject: [PATCH 293/536] Fix wrong docstring --- backend/src/app/schemas/editions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/schemas/editions.py b/backend/src/app/schemas/editions.py index 56f76e1a3..f1b0d366b 100644 --- a/backend/src/app/schemas/editions.py +++ b/backend/src/app/schemas/editions.py @@ -11,7 +11,7 @@ class EditionBase(CamelCaseModel): @validator("name") def valid_format(cls, v): - """Check that the email is of a valid format""" + """Check that the edition name is of a valid format""" validate_edition(v) return v From 25a70bfc1aa59456d4f65bf15d599cf2b1e585bb Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 8 Apr 2022 19:56:26 +0200 Subject: [PATCH 294/536] delete unnecessary import line --- backend/src/app/logic/suggestions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py index 3b2d183e0..321683770 100644 --- a/backend/src/app/logic/suggestions.py +++ b/backend/src/app/logic/suggestions.py @@ -1,4 +1,3 @@ -from setuptools import SetuptoolsDeprecationWarning from sqlalchemy.orm import Session from src.app.schemas.suggestion import NewSuggestion From e6ff3f4790ce4f0ef6cf671cff0c29fa8fd5c744 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Fri, 8 Apr 2022 20:03:27 +0200 Subject: [PATCH 295/536] added userId to auth context --- frontend/src/contexts/auth-context.tsx | 7 +++++++ .../src/views/VerifyingTokenPage/VerifyingTokenPage.tsx | 1 + 2 files changed, 8 insertions(+) diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx index 84b8c3fc2..03b94021b 100644 --- a/frontend/src/contexts/auth-context.tsx +++ b/frontend/src/contexts/auth-context.tsx @@ -11,6 +11,8 @@ export interface AuthContextState { setIsLoggedIn: (value: boolean | null) => void; role: Role | null; setRole: (value: Role | null) => void; + userId: number | null; + setUserId: (value: number | null) => void; token: string | null; setToken: (value: string | null) => void; editions: string[]; @@ -28,6 +30,8 @@ function authDefaultState(): AuthContextState { setIsLoggedIn: (_: boolean | null) => {}, role: null, setRole: (_: Role | null) => {}, + userId: null, + setUserId: (value: number | null) => {}, token: getToken(), setToken: (_: string | null) => {}, editions: [], @@ -55,6 +59,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { const [isLoggedIn, setIsLoggedIn] = useState(null); const [role, setRole] = useState(null); const [editions, setEditions] = useState([]); + const [userId, setUserId] = useState(null); // Default value: check LocalStorage const [token, setToken] = useState(getToken()); @@ -64,6 +69,8 @@ export function AuthProvider({ children }: { children: ReactNode }) { setIsLoggedIn: setIsLoggedIn, role: role, setRole: setRole, + userId: userId, + setUserId: setUserId, token: token, setToken: (value: string | null) => { // Log the user out if token is null diff --git a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx index cdbd7f116..a4b0334b4 100644 --- a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx +++ b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx @@ -26,6 +26,7 @@ export default function VerifyingTokenPage() { setBearerToken(authContext.token); authContext.setIsLoggedIn(true); authContext.setRole(response.admin ? Role.ADMIN : Role.COACH); + authContext.setUserId(response.userId) authContext.setEditions(response.editions); } }; From 5d7f66eeac4f67d6fb1604fa68b599ca268b4abf Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Fri, 8 Apr 2022 20:05:45 +0200 Subject: [PATCH 296/536] lint --- frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx index a4b0334b4..6c95c7afc 100644 --- a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx +++ b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx @@ -26,7 +26,7 @@ export default function VerifyingTokenPage() { setBearerToken(authContext.token); authContext.setIsLoggedIn(true); authContext.setRole(response.admin ? Role.ADMIN : Role.COACH); - authContext.setUserId(response.userId) + authContext.setUserId(response.userId); authContext.setEditions(response.editions); } }; From bee29a809351d708ce544cf868ea597ebdb0087e Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Fri, 8 Apr 2022 20:17:51 +0200 Subject: [PATCH 297/536] added userid to auth context when login in --- frontend/src/utils/api/login.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/utils/api/login.ts b/frontend/src/utils/api/login.ts index 61762c64b..7570b4aa1 100644 --- a/frontend/src/utils/api/login.ts +++ b/frontend/src/utils/api/login.ts @@ -8,6 +8,7 @@ interface LoginResponse { user: { admin: boolean; editions: string[]; + userId: number; }; } @@ -29,6 +30,7 @@ export async function logIn(auth: AuthContextState, email: string, password: str auth.setToken(login.accessToken); auth.setIsLoggedIn(true); auth.setRole(login.user.admin ? Role.ADMIN : Role.COACH); + auth.setUserId(login.user.userId); auth.setEditions(login.user.editions); return true; From 44732c485e8550ce61f87c7885a4413f371d6178 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 8 Apr 2022 20:25:02 +0200 Subject: [PATCH 298/536] refactor so it works with the changes --- .../app/routers/editions/students/students.py | 6 +- .../test_students/test_students.py | 198 ++++++++++-------- 2 files changed, 114 insertions(+), 90 deletions(-) diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index 0c9200148..36bd8db0b 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -3,7 +3,7 @@ from starlette import status from src.app.routers.tags import Tags -from src.app.utils.dependencies import get_student, get_edition, require_admin, require_authorization +from src.app.utils.dependencies import get_student, get_edition, require_admin, require_auth from src.app.logic.students import definitive_decision_on_student, remove_student, get_student_return, get_students_search from src.app.schemas.students import NewDecision, CommonQueryParams, ReturnStudent, ReturnStudentList from src.database.database import get_session @@ -15,7 +15,7 @@ students_suggestions_router, prefix="/{student_id}") -@students_router.get("/", dependencies=[Depends(require_authorization)]) +@students_router.get("/", dependencies=[Depends(require_auth)]) async def get_students(db: Session = Depends(get_session), commons: CommonQueryParams = Depends(CommonQueryParams), edition: Edition = Depends(get_edition)) -> ReturnStudentList: @@ -41,7 +41,7 @@ async def delete_student(student: Student = Depends(get_student), db: Session = remove_student(db, student) -@students_router.get("/{student_id}", dependencies=[Depends(require_authorization)]) +@students_router.get("/{student_id}", dependencies=[Depends(require_auth)]) async def get_student_by_id(edition: Edition = Depends(get_edition), student: Student = Depends(get_student)) -> ReturnStudent: """ Get information about a specific student. 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 374dc9b1f..212fd613a 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,40 +1,43 @@ import pytest from sqlalchemy.orm import Session from starlette import status -from starlette.testclient import TestClient from src.database.enums import DecisionEnum from src.database.models import Student, User, Edition, Skill, AuthEmail from src.app.logic.security import get_password_hash +from tests.utils.authorization import AuthClient + @pytest.fixture def database_with_data(database_session: Session) -> Session: """A fixture to fill the database with fake data that can easly be used when testing""" # Editions - edition: Edition = Edition(year=2022) + edition: Edition = Edition(year=2022, name="ed2022") database_session.add(edition) + database_session.commit() # Users - admin: User = User(name="admin", email="admin@ngmail.com", admin=True) - coach1: User = User(name="coach1", email="coach1@noutlook.be") - coach2: User = User(name="coach2", email="coach2@noutlook.be") - request: User = User(name="request", email="request@ngmail.com") + admin: User = User(name="admin", admin=True, editions=[edition]) + coach1: User = User(name="coach1", editions=[edition]) + coach2: User = User(name="coach2", editions=[edition]) database_session.add(admin) database_session.add(coach1) database_session.add(coach2) - database_session.add(request) + database_session.commit() # AuthEmail pw_hash = get_password_hash("wachtwoord") - auth_email_admin: AuthEmail = AuthEmail(user=admin, pw_hash=pw_hash) - auth_email_coach1: AuthEmail = AuthEmail(user=coach1, pw_hash=pw_hash) - auth_email_coach2: AuthEmail = AuthEmail(user=coach2, pw_hash=pw_hash) - auth_email_request: AuthEmail = AuthEmail(user=request, pw_hash=pw_hash) + auth_email_admin: AuthEmail = AuthEmail( + user=admin, email="admin@ngmail.com", pw_hash=pw_hash) + auth_email_coach1: AuthEmail = AuthEmail( + user=coach1, email="coach1@noutlook.be", pw_hash=pw_hash) + auth_email_coach2: AuthEmail = AuthEmail( + user=coach2, email="coach2@noutlook.be", pw_hash=pw_hash) database_session.add(auth_email_admin) database_session.add(auth_email_coach1) database_session.add(auth_email_coach2) - database_session.add(auth_email_request) + database_session.commit() # Skill skill1: Skill = Skill(name="skill1", description="something about skill1") @@ -49,6 +52,7 @@ def database_with_data(database_session: Session) -> Session: 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", @@ -60,223 +64,243 @@ def database_with_data(database_session: Session) -> Session: database_session.add(student01) database_session.add(student30) - database_session.commit() return database_session @pytest.fixture -def auth_coach1(test_client: TestClient) -> str: +def auth_coach1(auth_client: AuthClient) -> str: """A fixture for logging in coach1""" form = { "username": "coach1@noutlook.be", "password": "wachtwoord" } - token = test_client.post("/login/token", data=form).json()["accessToken"] + token = auth_client.post("/login/token", data=form).json()["accessToken"] auth = "Bearer " + token return auth @pytest.fixture -def auth_coach2(test_client: TestClient) -> str: +def auth_coach2(auth_client: AuthClient) -> str: """A fixture for logging in coach1""" form = { "username": "coach2@noutlook.be", "password": "wachtwoord" } - token = test_client.post("/login/token", data=form).json()["accessToken"] + token = auth_client.post("/login/token", data=form).json()["accessToken"] auth = "Bearer " + token return auth @pytest.fixture -def auth_admin(test_client: TestClient) -> str: +def auth_admin(auth_client: AuthClient) -> str: """A fixture for logging in admin""" form = { "username": "admin@ngmail.com", "password": "wachtwoord" } - token = test_client.post("/login/token", data=form).json()["accessToken"] + token = auth_client.post("/login/token", data=form).json()["accessToken"] auth = "Bearer " + token return auth -def test_set_definitive_decision_no_authorization(database_with_data: Session, test_client: TestClient): +def test_set_definitive_decision_no_authorization(database_with_data: Session, auth_client: AuthClient): """tests""" - assert test_client.put("/editions/1/students/2/decision", headers={ - "Authorization": "auth"}).status_code == status.HTTP_401_UNAUTHORIZED + assert auth_client.put( + "/editions/ed2022/students/2/decision").status_code == status.HTTP_401_UNAUTHORIZED -def test_set_definitive_decision_coach(database_with_data: Session, test_client: TestClient, auth_coach1): +def test_set_definitive_decision_coach(database_with_data: Session, auth_client: AuthClient, auth_coach1): """tests""" - assert test_client.put("/editions/1/students/2/decision", headers={ - "Authorization": auth_coach1}).status_code == status.HTTP_403_FORBIDDEN + 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 -def test_set_definitive_decision_on_ghost(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_set_definitive_decision_on_ghost(database_with_data: Session, auth_client: AuthClient): """tests""" - assert test_client.put("/editions/1/students/100/decision", - headers={"Authorization": auth_admin}).status_code == status.HTTP_404_NOT_FOUND + auth_client.admin() + assert auth_client.put( + "/editions/ed2022/students/100/decision").status_code == status.HTTP_404_NOT_FOUND -def test_set_definitive_decision_wrong_body(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_set_definitive_decision_wrong_body(database_with_data: Session, auth_client: AuthClient): """tests""" - assert test_client.put("/editions/1/students/1/decision", - headers={"Authorization": auth_admin}).status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + auth_client.admin() + assert auth_client.put( + "/editions/ed2022/students/1/decision").status_code == status.HTTP_422_UNPROCESSABLE_ENTITY -def test_set_definitive_decision_yes(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_set_definitive_decision_yes(database_with_data: Session, auth_client: AuthClient): """tests""" - assert test_client.put("/editions/1/students/1/decision", - headers={"Authorization": auth_admin}, + 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 -def test_set_definitive_decision_no(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_set_definitive_decision_no(database_with_data: Session, auth_client: AuthClient): """tests""" - assert test_client.put("/editions/1/students/1/decision", - headers={"Authorization": auth_admin}, + 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 -def test_set_definitive_decision_maybe(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_set_definitive_decision_maybe(database_with_data: Session, auth_client: AuthClient): """tests""" - assert test_client.put("/editions/1/students/1/decision", - headers={"Authorization": auth_admin}, + 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 -def test_delete_student_no_authorization(database_with_data: Session, test_client: TestClient): +def test_delete_student_no_authorization(database_with_data: Session, auth_client: AuthClient): """tests""" - assert test_client.delete("/editions/1/students/2", headers={ + assert auth_client.delete("/editions/ed2022/students/2", headers={ "Authorization": "auth"}).status_code == status.HTTP_401_UNAUTHORIZED -def test_delete_student_coach(database_with_data: Session, test_client: TestClient, auth_coach1): +def test_delete_student_coach(database_with_data: Session, auth_client: AuthClient, auth_coach1): """tests""" - assert test_client.delete("/editions/1/students/2", headers={ - "Authorization": auth_coach1}).status_code == status.HTTP_403_FORBIDDEN + 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 -def test_delete_ghost(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_delete_ghost(database_with_data: Session, auth_client: AuthClient): """tests""" - assert test_client.delete("/editions/1/students/100", - headers={"Authorization": auth_admin}).status_code == status.HTTP_404_NOT_FOUND + 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 -def test_delete(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_delete(database_with_data: Session, auth_client: AuthClient): """tests""" - assert test_client.delete("/editions/1/students/1", - headers={"Authorization": auth_admin}).status_code == status.HTTP_204_NO_CONTENT + 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 -def test_get_student_by_id_no_autorization(database_with_data: Session, test_client: TestClient): +def test_get_student_by_id_no_autorization(database_with_data: Session, auth_client: AuthClient): """tests""" - assert test_client.get("/editions/1/students/1", - headers={"Authorization": "auth_admin"}).status_code == status.HTTP_401_UNAUTHORIZED + assert auth_client.get( + "/editions/ed2022/students/1").status_code == status.HTTP_401_UNAUTHORIZED -def test_get_student_by_id(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_get_student_by_id(database_with_data: Session, auth_client: AuthClient): """tests""" - assert test_client.get("/editions/1/students/1", - headers={"Authorization": auth_admin}).status_code == status.HTTP_200_OK + auth_client.admin() + assert auth_client.get( + "/editions/ed2022/students/1").status_code == status.HTTP_200_OK -def test_get_students_no_autorization(database_with_data: Session, test_client: TestClient): +def test_get_students_no_autorization(database_with_data: Session, auth_client: AuthClient): """tests""" - assert test_client.get("/editions/1/students/", - headers={"Authorization": "auth_admin"}).status_code == status.HTTP_401_UNAUTHORIZED + assert auth_client.get( + "/editions/ed2022/students/").status_code == status.HTTP_401_UNAUTHORIZED -def test_get_all_students(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_get_all_students(database_with_data: Session, auth_client: AuthClient): """tests""" - response = test_client.get("/editions/1/students/", - headers={"Authorization": auth_admin}) + 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 -def test_get_first_name_students(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_get_first_name_students(database_with_data: Session, auth_client: AuthClient): """tests""" - response = test_client.get("/editions/1/students/?first_name=Jos", - headers={"Authorization": auth_admin}) + edition: Edition = database_with_data.query(Edition).all()[0] + auth_client.coach(edition) + response = auth_client.get("/editions/ed2022/students/?first_name=Jos") assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 1 -def test_get_last_name_students(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_get_last_name_students(database_with_data: Session, auth_client: AuthClient): """tests""" - response = test_client.get("/editions/1/students/?last_name=Vermeulen", - headers={"Authorization": auth_admin}) + edition: Edition = database_with_data.query(Edition).all()[0] + auth_client.coach(edition) + response = auth_client.get( + "/editions/ed2022/students/?last_name=Vermeulen") assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 1 -def test_get_alumni_students(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_get_alumni_students(database_with_data: Session, auth_client: AuthClient): """tests""" - response = test_client.get("/editions/1/students/?alumni=true", - headers={"Authorization": auth_admin}) + 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 -def test_get_student_coach_students(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_get_student_coach_students(database_with_data: Session, auth_client: AuthClient): """tests""" - response = test_client.get("/editions/1/students/?student_coach=true", - headers={"Authorization": auth_admin}) + edition: Edition = database_with_data.query(Edition).all()[0] + auth_client.coach(edition) + response = auth_client.get("/editions/ed2022/students/?student_coach=true") assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 1 -def test_get_one_skill_students(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_get_one_skill_students(database_with_data: Session, auth_client: AuthClient): """tests""" - response = test_client.get("/editions/1/students/?skill_ids=1", - headers={"Authorization": auth_admin}) + edition: Edition = database_with_data.query(Edition).all()[0] + auth_client.coach(edition) + response = auth_client.get("/editions/ed2022/students/?skill_ids=1") 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, test_client: TestClient, auth_admin: str): +def test_get_multiple_skill_students(database_with_data: Session, auth_client: AuthClient): """tests""" - response = test_client.get("/editions/1/students/?skill_ids=4&skill_ids=5", - headers={"Authorization": auth_admin}) + 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") 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, test_client: TestClient, auth_admin: str): +def test_get_multiple_skill_students_no_students(database_with_data: Session, auth_client: AuthClient): """tests""" - response = test_client.get("/editions/1/students/?skill_ids=4&skill_ids=6", - headers={"Authorization": auth_admin}) + 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") assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 0 -def test_get_ghost_skill_students(database_with_data: Session, test_client: TestClient, auth_admin: str): +def test_get_ghost_skill_students(database_with_data: Session, auth_client: AuthClient): """tests""" - response = test_client.get("/editions/1/students/?skill_ids=100", - headers={"Authorization": auth_admin}) + edition: Edition = database_with_data.query(Edition).all()[0] + auth_client.coach(edition) + response = auth_client.get("/editions/ed2022/students/?skill_ids=100") assert response.status_code == status.HTTP_404_NOT_FOUND From 78f5823e09efca7854c2bfaf4a959c7eba52af65 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Fri, 8 Apr 2022 20:31:34 +0200 Subject: [PATCH 299/536] alembic upgrade --- backend/migrations/versions/964637070800_.py | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 backend/migrations/versions/964637070800_.py diff --git a/backend/migrations/versions/964637070800_.py b/backend/migrations/versions/964637070800_.py new file mode 100644 index 000000000..9c4080542 --- /dev/null +++ b/backend/migrations/versions/964637070800_.py @@ -0,0 +1,24 @@ +"""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 From a140fcb2a611ea21b7082394b44fd2cfa15a9191 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 8 Apr 2022 16:23:35 +0200 Subject: [PATCH 300/536] Fix image paths & missing images --- frontend/public/assets/osoc_logo_dark.svg | 14 +++ frontend/public/assets/osoc_logo_letters.svg | 38 ++++++++ frontend/public/assets/osoc_logo_light.svg | 82 ++++++++++++++++++ frontend/public/index.html | 2 +- frontend/public/logo192.png | Bin 0 -> 10137 bytes frontend/public/logo512.png | Bin 0 -> 29097 bytes frontend/public/manifest.json | 4 +- frontend/src/Router.tsx | 4 +- frontend/src/components/Navbar/NavBar.tsx | 22 +++++ .../components/{navbar => Navbar}/index.ts | 0 frontend/src/components/index.ts | 2 +- .../{navbar => navbar_old}/NavBarElements.tsx | 0 .../NavBar.tsx => navbar_old/NavBarOld.tsx} | 2 +- frontend/src/components/navbar_old/index.ts | 1 + .../{navbar => navbar_old}/navbar.css | 0 15 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 frontend/public/assets/osoc_logo_dark.svg create mode 100644 frontend/public/assets/osoc_logo_letters.svg create mode 100644 frontend/public/assets/osoc_logo_light.svg create mode 100644 frontend/public/logo192.png create mode 100644 frontend/public/logo512.png create mode 100644 frontend/src/components/Navbar/NavBar.tsx rename frontend/src/components/{navbar => Navbar}/index.ts (100%) rename frontend/src/components/{navbar => navbar_old}/NavBarElements.tsx (100%) rename frontend/src/components/{navbar/NavBar.tsx => navbar_old/NavBarOld.tsx} (97%) create mode 100644 frontend/src/components/navbar_old/index.ts rename frontend/src/components/{navbar => navbar_old}/navbar.css (100%) diff --git a/frontend/public/assets/osoc_logo_dark.svg b/frontend/public/assets/osoc_logo_dark.svg new file mode 100644 index 000000000..9730c2fdd --- /dev/null +++ b/frontend/public/assets/osoc_logo_dark.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/public/assets/osoc_logo_letters.svg b/frontend/public/assets/osoc_logo_letters.svg new file mode 100644 index 000000000..f03945a52 --- /dev/null +++ b/frontend/public/assets/osoc_logo_letters.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/public/assets/osoc_logo_light.svg b/frontend/public/assets/osoc_logo_light.svg new file mode 100644 index 000000000..e1f9823a0 --- /dev/null +++ b/frontend/public/assets/osoc_logo_light.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/public/index.html b/frontend/public/index.html index 9cfe9c5a7..b2edc6981 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -7,7 +7,7 @@ + + + + + + + + + + + + + + + + diff --git a/frontend/public/assets/osoc_logo_letters.svg b/frontend/public/assets/osoc_logo_letters_light.svg similarity index 100% rename from frontend/public/assets/osoc_logo_letters.svg rename to frontend/public/assets/osoc_logo_letters_light.svg diff --git a/frontend/src/components/Footer/styles.ts b/frontend/src/components/Footer/styles.ts index ce8bf963c..b8a9bee8c 100644 --- a/frontend/src/components/Footer/styles.ts +++ b/frontend/src/components/Footer/styles.ts @@ -11,6 +11,7 @@ export const FooterTitle = styled.h3` export const FooterLink = styled.a` color: white; + transition: 200ms ease-out; &:hover { color: var(--osoc_green); diff --git a/frontend/src/components/Navbar/Brand.tsx b/frontend/src/components/Navbar/Brand.tsx index 116b804b1..d04a25b87 100644 --- a/frontend/src/components/Navbar/Brand.tsx +++ b/frontend/src/components/Navbar/Brand.tsx @@ -4,9 +4,9 @@ export default function Brand() { return ( {"OSOC{" "} diff --git a/frontend/src/components/Navbar/EditionDropdown.tsx b/frontend/src/components/Navbar/EditionDropdown.tsx index 025b9f112..e0ef7906d 100644 --- a/frontend/src/components/Navbar/EditionDropdown.tsx +++ b/frontend/src/components/Navbar/EditionDropdown.tsx @@ -1,5 +1,6 @@ import React from "react"; -import Dropdown from "react-bootstrap/esm/Dropdown"; +import NavDropdown from "react-bootstrap/NavDropdown"; +import { StyledDropdownItem } from "./styles"; interface Props { editions: string[]; @@ -10,9 +11,14 @@ interface Props { export default function EditionDropdown(props: Props) { const navItems: React.ReactNode[] = []; + // Load dropdown items dynamically props.editions.forEach((edition: string) => { - navItems.push({edition}); + navItems.push( + + {edition} + + ); }); - return {navItems}; + return {navItems}; } diff --git a/frontend/src/components/Navbar/NavBar.css b/frontend/src/components/Navbar/NavBar.css new file mode 100644 index 000000000..27a45c2ac --- /dev/null +++ b/frontend/src/components/Navbar/NavBar.css @@ -0,0 +1,5 @@ +.dropdown-menu { + background-color: var(--osoc_blue); + max-height: 200px; + overflow: auto; +} \ No newline at end of file diff --git a/frontend/src/components/Navbar/NavBar.tsx b/frontend/src/components/Navbar/NavBar.tsx index 31a5d70b2..cd41d0bff 100644 --- a/frontend/src/components/Navbar/NavBar.tsx +++ b/frontend/src/components/Navbar/NavBar.tsx @@ -3,14 +3,18 @@ import { BSNavbar } from "./styles"; import { useAuth } from "../../contexts/auth-context"; import Brand from "./Brand"; import Nav from "react-bootstrap/Nav"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import NavDropdown from "react-bootstrap/NavDropdown"; import EditionDropdown from "./EditionDropdown"; +import "./NavBar.css"; export default function Navbar() { const { isLoggedIn, editions } = useAuth(); - const [currentEdition, setCurrentEdition] = useState("edition"); - // const [currentEdition, setCurrentEdition] = useState(editions[0]); + const [currentEdition, setCurrentEdition] = useState(editions[0]); + + useEffect(() => { + setCurrentEdition(editions[0]); + }, [editions]); // Don't render Navbar if not logged in if (!isLoggedIn) { @@ -19,7 +23,7 @@ export default function Navbar() { return ( - + {/* Make Navbar responsive (hamburger menu) */} diff --git a/frontend/src/components/Navbar/styles.ts b/frontend/src/components/Navbar/styles.ts index a8b6ccc01..c36e41974 100644 --- a/frontend/src/components/Navbar/styles.ts +++ b/frontend/src/components/Navbar/styles.ts @@ -15,7 +15,21 @@ export const BSBrand = styled(BSNavbar.Brand)` font-weight: bold; `; -export const Dropdown = styled(NavDropdown)` - background-color: var(--osoc_blue); +export const StyledDropdownItem = styled(NavDropdown.Item)` color: white; + transition: 200ms ease-out; + background-color: transparent; + + &:hover { + background-color: transparent; + color: var(--osoc_green); + transition: 200ms ease-in; + } + + &:active { + background-color: transparent; + color: var(--osoc_orange); + text-decoration: underline; + font-weight: bold; + } `; diff --git a/frontend/src/utils/api/login.ts b/frontend/src/utils/api/login.ts index 7570b4aa1..575d3870d 100644 --- a/frontend/src/utils/api/login.ts +++ b/frontend/src/utils/api/login.ts @@ -2,14 +2,11 @@ import axios from "axios"; import { axiosInstance } from "./api"; import { AuthContextState } from "../../contexts"; import { Role } from "../../data/enums"; +import { User } from "../../data/interfaces"; interface LoginResponse { accessToken: string; - user: { - admin: boolean; - editions: string[]; - userId: number; - }; + user: User; } /** From 934fd77b8fb8d3ab6fa568284c4c6c9ccc6e3d6f Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 8 Apr 2022 20:21:23 +0200 Subject: [PATCH 306/536] Create logout button & fix small issues in editions --- .../src/components/Navbar/EditionDropdown.tsx | 2 +- .../src/components/Navbar/LogoutButton.tsx | 24 +++++++++++++++++++ frontend/src/components/Navbar/NavBar.css | 5 ++++ frontend/src/components/Navbar/NavBar.tsx | 10 ++++---- frontend/src/components/Navbar/styles.ts | 15 ++++++++---- frontend/src/views/LoginPage/LoginPage.tsx | 4 ++-- 6 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 frontend/src/components/Navbar/LogoutButton.tsx diff --git a/frontend/src/components/Navbar/EditionDropdown.tsx b/frontend/src/components/Navbar/EditionDropdown.tsx index e0ef7906d..80f1f0b53 100644 --- a/frontend/src/components/Navbar/EditionDropdown.tsx +++ b/frontend/src/components/Navbar/EditionDropdown.tsx @@ -14,7 +14,7 @@ export default function EditionDropdown(props: Props) { // Load dropdown items dynamically props.editions.forEach((edition: string) => { navItems.push( - + {edition} ); diff --git a/frontend/src/components/Navbar/LogoutButton.tsx b/frontend/src/components/Navbar/LogoutButton.tsx new file mode 100644 index 000000000..89d1ff01a --- /dev/null +++ b/frontend/src/components/Navbar/LogoutButton.tsx @@ -0,0 +1,24 @@ +import { LogOutText } from "./styles"; +import { useAuth } from "../../contexts/auth-context"; +import { useNavigate } from "react-router-dom"; + +export default function LogoutButton() { + const authContext = useAuth(); + const navigate = useNavigate(); + + /** + * Log the user out + */ + function handleLogout() { + // Unset auth state + authContext.setIsLoggedIn(false); + authContext.setEditions([]); + authContext.setRole(null); + authContext.setToken(null); + + // Redirect to login page + navigate("/login"); + } + + return Log Out; +} diff --git a/frontend/src/components/Navbar/NavBar.css b/frontend/src/components/Navbar/NavBar.css index 27a45c2ac..4d68a3b19 100644 --- a/frontend/src/components/Navbar/NavBar.css +++ b/frontend/src/components/Navbar/NavBar.css @@ -2,4 +2,9 @@ background-color: var(--osoc_blue); max-height: 200px; overflow: auto; +} + +.dropdown-item.active { + background-color: transparent; + text-decoration: underline; } \ No newline at end of file diff --git a/frontend/src/components/Navbar/NavBar.tsx b/frontend/src/components/Navbar/NavBar.tsx index cd41d0bff..4e05e519b 100644 --- a/frontend/src/components/Navbar/NavBar.tsx +++ b/frontend/src/components/Navbar/NavBar.tsx @@ -1,5 +1,5 @@ import Container from "react-bootstrap/esm/Container"; -import { BSNavbar } from "./styles"; +import { BSNavbar, StyledDropdownItem } from "./styles"; import { useAuth } from "../../contexts/auth-context"; import Brand from "./Brand"; import Nav from "react-bootstrap/Nav"; @@ -7,6 +7,7 @@ import { useEffect, useState } from "react"; import NavDropdown from "react-bootstrap/NavDropdown"; import EditionDropdown from "./EditionDropdown"; import "./NavBar.css"; +import LogoutButton from "./LogoutButton"; export default function Navbar() { const { isLoggedIn, editions } = useAuth(); @@ -38,11 +39,12 @@ export default function Navbar() { Projects Students - Admins - + Admins + Coaches - + + diff --git a/frontend/src/components/Navbar/styles.ts b/frontend/src/components/Navbar/styles.ts index c36e41974..f65abd5eb 100644 --- a/frontend/src/components/Navbar/styles.ts +++ b/frontend/src/components/Navbar/styles.ts @@ -25,11 +25,16 @@ export const StyledDropdownItem = styled(NavDropdown.Item)` color: var(--osoc_green); transition: 200ms ease-in; } +`; - &:active { - background-color: transparent; - color: var(--osoc_orange); - text-decoration: underline; - font-weight: bold; +export const LogOutText = styled(BSNavbar.Text).attrs(() => ({ + className: "ms-2", +}))` + transition: 150ms ease-out; + + &:hover { + cursor: pointer; + color: rgba(255, 255, 255, 75%); + transition: 150ms ease-in; } `; diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index 610e5aebc..ac5e67a28 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -26,7 +26,7 @@ export default function LoginPage() { useEffect(() => { // If the user is already logged in, redirect them to - // the "students" page instead of showing the login page + // the "editions" page instead of showing the login page if (authCtx.isLoggedIn) { navigate("/editions"); } @@ -35,7 +35,7 @@ export default function LoginPage() { async function callLogIn() { try { const response = await logIn(authCtx, email, password); - if (response) navigate("/students"); + if (response) navigate("/editions"); else alert("Something went wrong when login in"); } catch (error) { console.log(error); From f7095b8fad01b4f701035946c3a87035c85f1556 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 8 Apr 2022 20:41:36 +0200 Subject: [PATCH 307/536] Refactor auth code --- frontend/src/contexts/auth-context.tsx | 29 ++++++++++++++++++- frontend/src/contexts/index.ts | 2 +- frontend/src/utils/api/login.ts | 9 ++---- .../VerifyingTokenPage/VerifyingTokenPage.tsx | 12 ++------ 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx index 03b94021b..2817b371d 100644 --- a/frontend/src/contexts/auth-context.tsx +++ b/frontend/src/contexts/auth-context.tsx @@ -2,6 +2,8 @@ import { Role } from "../data/enums"; import React, { useContext, ReactNode, useState } from "react"; import { getToken, setToken as setTokenInStorage } from "../utils/local-storage"; +import { User } from "../data/interfaces"; +import { setBearerToken } from "../utils/api"; /** * Interface that holds the data stored in the AuthContext. @@ -31,7 +33,7 @@ function authDefaultState(): AuthContextState { role: null, setRole: (_: Role | null) => {}, userId: null, - setUserId: (value: number | null) => {}, + setUserId: (_: number | null) => {}, token: getToken(), setToken: (_: string | null) => {}, editions: [], @@ -81,6 +83,9 @@ export function AuthProvider({ children }: { children: ReactNode }) { // Set the token in LocalStorage setTokenInStorage(value); setToken(value); + + // Set token in request headers + setBearerToken(value); }, editions: editions, setEditions: setEditions, @@ -88,3 +93,25 @@ export function AuthProvider({ children }: { children: ReactNode }) { return {children}; } + +/** + * Set the user's login data in the AuthContext + */ +export function logIn(user: User, token: string | null, authContext: AuthContextState) { + authContext.setIsLoggedIn(true); + authContext.setUserId(user.userId); + authContext.setRole(user.admin ? Role.ADMIN : Role.COACH); + authContext.setEditions(user.editions); + authContext.setToken(token); +} + +/** + * Remove a user's login data from the AuthContext + */ +export function logOut(authContext: AuthContextState) { + authContext.setIsLoggedIn(false); + authContext.setUserId(null); + authContext.setRole(null); + authContext.setEditions([]); + authContext.setToken(null); +} diff --git a/frontend/src/contexts/index.ts b/frontend/src/contexts/index.ts index 067ec1341..133c24ae4 100644 --- a/frontend/src/contexts/index.ts +++ b/frontend/src/contexts/index.ts @@ -1,3 +1,3 @@ import type { AuthContextState } from "./auth-context"; export type { AuthContextState }; -export { AuthProvider } from "./auth-context"; +export { AuthProvider, logIn, logOut, useAuth } from "./auth-context"; diff --git a/frontend/src/utils/api/login.ts b/frontend/src/utils/api/login.ts index 575d3870d..2eed1840a 100644 --- a/frontend/src/utils/api/login.ts +++ b/frontend/src/utils/api/login.ts @@ -1,7 +1,6 @@ import axios from "axios"; import { axiosInstance } from "./api"; -import { AuthContextState } from "../../contexts"; -import { Role } from "../../data/enums"; +import { AuthContextState, logIn as ctxLogIn } from "../../contexts"; import { User } from "../../data/interfaces"; interface LoginResponse { @@ -24,11 +23,7 @@ export async function logIn(auth: AuthContextState, email: string, password: str try { const response = await axiosInstance.post("/login/token", payload); const login = response.data as LoginResponse; - auth.setToken(login.accessToken); - auth.setIsLoggedIn(true); - auth.setRole(login.user.admin ? Role.ADMIN : Role.COACH); - auth.setUserId(login.user.userId); - auth.setEditions(login.user.editions); + ctxLogIn(login.user, login.accessToken, auth); return true; } catch (error) { diff --git a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx index 6c95c7afc..63e6f548f 100644 --- a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx +++ b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx @@ -1,9 +1,6 @@ import { useEffect } from "react"; - -import { setBearerToken } from "../../utils/api"; import { validateBearerToken } from "../../utils/api/auth"; -import { Role } from "../../data/enums"; -import { useAuth } from "../../contexts/auth-context"; +import { logIn, useAuth } from "../../contexts"; /** * Placeholder page shown while the bearer token found in LocalStorage is being verified. @@ -23,11 +20,8 @@ export default function VerifyingTokenPage() { authContext.setEditions([]); } else { // Token was valid, use it as the default request header - setBearerToken(authContext.token); - authContext.setIsLoggedIn(true); - authContext.setRole(response.admin ? Role.ADMIN : Role.COACH); - authContext.setUserId(response.userId); - authContext.setEditions(response.editions); + // and set all data in the AuthContext + logIn(response, authContext.token, authContext); } }; From 76468a153844b29811ead0dd3208c09fbf983a76 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 8 Apr 2022 21:09:31 +0200 Subject: [PATCH 308/536] Finish dropdown, return editions in reverse order --- backend/src/database/crud/users.py | 3 ++- .../src/components/Navbar/EditionDropdown.tsx | 19 ++++++++++++++++++- .../VerifyingTokenPage/VerifyingTokenPage.tsx | 7 ++----- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index c826a5cbd..94edf66ae 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -33,7 +33,8 @@ def get_user_edition_names(db: Session, user: User) -> list[str]: # Name is 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 - for edition in source: + # Return from newest to oldest + for edition in sorted(source, key=lambda e: e.year, reverse=True): if edition.name is not None: editions.append(edition.name) diff --git a/frontend/src/components/Navbar/EditionDropdown.tsx b/frontend/src/components/Navbar/EditionDropdown.tsx index 80f1f0b53..84dcece47 100644 --- a/frontend/src/components/Navbar/EditionDropdown.tsx +++ b/frontend/src/components/Navbar/EditionDropdown.tsx @@ -1,6 +1,7 @@ import React from "react"; import NavDropdown from "react-bootstrap/NavDropdown"; import { StyledDropdownItem } from "./styles"; +import { useNavigate } from "react-router-dom"; interface Props { editions: string[]; @@ -10,11 +11,27 @@ interface Props { export default function EditionDropdown(props: Props) { const navItems: React.ReactNode[] = []; + const navigate = useNavigate(); + + /** + * Change the route based on the edition + * This can't be a separate function because it uses hooks which may + * only be used in React components + */ + function handleSelect(edition: string) { + // TODO: Navigate to the most specific route possible for QOL? + // eg. /editions/old_id/students/:id => /editions/new_id/students, etc + navigate(`/editions/${edition}`); + } // Load dropdown items dynamically props.editions.forEach((edition: string) => { navItems.push( - + handleSelect(edition)} + > {edition} ); diff --git a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx index 63e6f548f..ea6737f48 100644 --- a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx +++ b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { validateBearerToken } from "../../utils/api/auth"; -import { logIn, useAuth } from "../../contexts"; +import { logIn, logOut, useAuth } from "../../contexts"; /** * Placeholder page shown while the bearer token found in LocalStorage is being verified. @@ -14,10 +14,7 @@ export default function VerifyingTokenPage() { const response = await validateBearerToken(authContext.token); if (response === null) { - authContext.setToken(null); - authContext.setIsLoggedIn(false); - authContext.setRole(null); - authContext.setEditions([]); + logOut(authContext); } else { // Token was valid, use it as the default request header // and set all data in the AuthContext From 8f6f33307ce19ddd0387807cd07d3f2ee04774ab Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 8 Apr 2022 21:17:21 +0200 Subject: [PATCH 309/536] Add docstrings & delete old code --- frontend/src/components/Navbar/Brand.tsx | 3 + .../src/components/Navbar/EditionDropdown.tsx | 3 + .../src/components/Navbar/LogoutButton.tsx | 10 +-- .../Navbar/{NavBar.css => Navbar.css} | 2 +- .../Navbar/{NavBar.tsx => Navbar.tsx} | 4 +- frontend/src/components/Navbar/index.ts | 2 +- .../components/navbar_old/NavBarElements.tsx | 82 ------------------- .../src/components/navbar_old/NavBarOld.tsx | 45 ---------- frontend/src/components/navbar_old/index.ts | 1 - frontend/src/components/navbar_old/navbar.css | 16 ---- 10 files changed, 15 insertions(+), 153 deletions(-) rename frontend/src/components/Navbar/{NavBar.css => Navbar.css} (98%) rename frontend/src/components/Navbar/{NavBar.tsx => Navbar.tsx} (96%) delete mode 100644 frontend/src/components/navbar_old/NavBarElements.tsx delete mode 100644 frontend/src/components/navbar_old/NavBarOld.tsx delete mode 100644 frontend/src/components/navbar_old/index.ts delete mode 100644 frontend/src/components/navbar_old/navbar.css diff --git a/frontend/src/components/Navbar/Brand.tsx b/frontend/src/components/Navbar/Brand.tsx index d04a25b87..4e7e79bc9 100644 --- a/frontend/src/components/Navbar/Brand.tsx +++ b/frontend/src/components/Navbar/Brand.tsx @@ -1,5 +1,8 @@ import { BSBrand } from "./styles"; +/** + * React component that shows the OSOC logo & title in the [[Navbar]] + */ export default function Brand() { return ( diff --git a/frontend/src/components/Navbar/EditionDropdown.tsx b/frontend/src/components/Navbar/EditionDropdown.tsx index 84dcece47..8d9fc79fb 100644 --- a/frontend/src/components/Navbar/EditionDropdown.tsx +++ b/frontend/src/components/Navbar/EditionDropdown.tsx @@ -9,6 +9,9 @@ interface Props { setCurrentEdition: (edition: string) => void; } +/** + * Dropdown in the [[Navbar]] to change the current edition to another one + */ export default function EditionDropdown(props: Props) { const navItems: React.ReactNode[] = []; const navigate = useNavigate(); diff --git a/frontend/src/components/Navbar/LogoutButton.tsx b/frontend/src/components/Navbar/LogoutButton.tsx index 89d1ff01a..b9945cfe1 100644 --- a/frontend/src/components/Navbar/LogoutButton.tsx +++ b/frontend/src/components/Navbar/LogoutButton.tsx @@ -1,7 +1,10 @@ import { LogOutText } from "./styles"; -import { useAuth } from "../../contexts/auth-context"; +import { logOut, useAuth } from "../../contexts"; import { useNavigate } from "react-router-dom"; +/** + * Button in the [[Navbar]] to log the user out + */ export default function LogoutButton() { const authContext = useAuth(); const navigate = useNavigate(); @@ -11,10 +14,7 @@ export default function LogoutButton() { */ function handleLogout() { // Unset auth state - authContext.setIsLoggedIn(false); - authContext.setEditions([]); - authContext.setRole(null); - authContext.setToken(null); + logOut(authContext); // Redirect to login page navigate("/login"); diff --git a/frontend/src/components/Navbar/NavBar.css b/frontend/src/components/Navbar/Navbar.css similarity index 98% rename from frontend/src/components/Navbar/NavBar.css rename to frontend/src/components/Navbar/Navbar.css index 4d68a3b19..2214a5c9d 100644 --- a/frontend/src/components/Navbar/NavBar.css +++ b/frontend/src/components/Navbar/Navbar.css @@ -7,4 +7,4 @@ .dropdown-item.active { background-color: transparent; text-decoration: underline; -} \ No newline at end of file +} diff --git a/frontend/src/components/Navbar/NavBar.tsx b/frontend/src/components/Navbar/Navbar.tsx similarity index 96% rename from frontend/src/components/Navbar/NavBar.tsx rename to frontend/src/components/Navbar/Navbar.tsx index 4e05e519b..e49aa6804 100644 --- a/frontend/src/components/Navbar/NavBar.tsx +++ b/frontend/src/components/Navbar/Navbar.tsx @@ -1,12 +1,12 @@ import Container from "react-bootstrap/esm/Container"; import { BSNavbar, StyledDropdownItem } from "./styles"; -import { useAuth } from "../../contexts/auth-context"; +import { useAuth } from "../../contexts"; import Brand from "./Brand"; import Nav from "react-bootstrap/Nav"; import { useEffect, useState } from "react"; import NavDropdown from "react-bootstrap/NavDropdown"; import EditionDropdown from "./EditionDropdown"; -import "./NavBar.css"; +import "./Navbar.css"; import LogoutButton from "./LogoutButton"; export default function Navbar() { diff --git a/frontend/src/components/Navbar/index.ts b/frontend/src/components/Navbar/index.ts index 3ab07fb62..8d95d6656 100644 --- a/frontend/src/components/Navbar/index.ts +++ b/frontend/src/components/Navbar/index.ts @@ -1 +1 @@ -export { default } from "./NavBar"; +export { default } from "./Navbar"; diff --git a/frontend/src/components/navbar_old/NavBarElements.tsx b/frontend/src/components/navbar_old/NavBarElements.tsx deleted file mode 100644 index 0712b05de..000000000 --- a/frontend/src/components/navbar_old/NavBarElements.tsx +++ /dev/null @@ -1,82 +0,0 @@ -// @ts-ignore -import styled from "styled-components"; -import { NavLink as Link } from "react-router-dom"; -import { FaBars } from "react-icons/fa"; -import "../../App.css"; - -export const Nav = styled.nav` - background: var(--osoc_blue); - height: 80px; - width: 100%; - display: flex; - position: relative; - justify-content: space-between; - padding: 0.5rem calc((100vw - 1000px) / 2); - z-index: 10; -`; - -export const NavLink = styled(Link)` - color: #fff; - display: flex; - align-items: center; - text-decoration: none; - padding: 0 1rem; - height: 100%; - cursor: pointer; - - &.active { - color: var(--osoc_red); - } -`; - -export const Bars = styled(FaBars)` - display: none; - color: #fff; - - @media screen and (max-width: 768px) { - display: block; - position: absolute; - top: 0; - right: 0; - transform: translate(-100%, 75%); - font-size: 1.8rem; - cursor: pointer; - } -`; - -export const NavMenu = styled.div` - display: flex; - margin-right: 30px; - - @media screen and (max-width: 768px) { - display: none; - } -`; - -export const NavBtn = styled.nav` - display: flex; - align-items: center; - margin-right: 24px; - - @media screen and (max-width: 768px) { - display: none; - } -`; - -export const NavBtnLink = styled(Link)` - border-radius: 4px; - background: var(--osoc_red); - padding: 10px 22px; - color: #fff; - border: none; - outline: none; - cursor: pointer; - transition: all 0.2s ease-in-out; - text-decoration: none; - - &:hover { - transition: all 0.2s ease-in-out; - background: #fff; - color: #010606; - } -`; diff --git a/frontend/src/components/navbar_old/NavBarOld.tsx b/frontend/src/components/navbar_old/NavBarOld.tsx deleted file mode 100644 index 6dc00f879..000000000 --- a/frontend/src/components/navbar_old/NavBarOld.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from "react"; -import { Bars, Nav, NavLink, NavMenu } from "./NavBarElements"; -import "./navbar.css"; -import { useAuth } from "../../contexts/auth-context"; - -/** - * NavBar displayed at the top of the page. - * Links are hidden if the user is not authorized to see them. - */ -export default function NavBarOld() { - const { token, setToken } = useAuth(); - const hidden = token ? "nav-links" : "nav-hidden"; - - return ( - <> - - - ); -} diff --git a/frontend/src/components/navbar_old/index.ts b/frontend/src/components/navbar_old/index.ts deleted file mode 100644 index 72314ee7b..000000000 --- a/frontend/src/components/navbar_old/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./NavBarOld"; diff --git a/frontend/src/components/navbar_old/navbar.css b/frontend/src/components/navbar_old/navbar.css deleted file mode 100644 index 36c48b56c..000000000 --- a/frontend/src/components/navbar_old/navbar.css +++ /dev/null @@ -1,16 +0,0 @@ -.nav-links { - display: flex; -} - -.nav-hidden { - visibility: hidden; -} - -.logo-plus-name { - color: #fff; - display: flex; - align-items: center; - text-decoration: none; - padding: 0 1rem; - height: 100%; -} From 6fb77194fb3c85c750be4d4593d6b582675172a1 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 8 Apr 2022 21:24:41 +0200 Subject: [PATCH 310/536] Remove margin from logout button --- frontend/src/components/Navbar/styles.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/components/Navbar/styles.ts b/frontend/src/components/Navbar/styles.ts index f65abd5eb..304fc980b 100644 --- a/frontend/src/components/Navbar/styles.ts +++ b/frontend/src/components/Navbar/styles.ts @@ -27,9 +27,7 @@ export const StyledDropdownItem = styled(NavDropdown.Item)` } `; -export const LogOutText = styled(BSNavbar.Text).attrs(() => ({ - className: "ms-2", -}))` +export const LogOutText = styled(BSNavbar.Text)` transition: 150ms ease-out; &:hover { From ad6b6534061aae19e94109eef983bbf5a9d63288 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Fri, 8 Apr 2022 21:25:52 +0200 Subject: [PATCH 311/536] started working on ProjectDetailPage added getProject api call --- frontend/src/Router.tsx | 4 ++-- .../ProjectCard/ProjectCard.tsx | 6 +++--- frontend/src/data/interfaces/projects.ts | 2 +- frontend/src/utils/api/projects.ts | 17 ++++++++++++++++- .../src/views/ProjectsPage/ProjectsPage.css | 0 frontend/src/views/index.ts | 3 ++- .../ProjectDetailPage/ProjectDetailPage.tsx | 8 ++++++++ .../projectViews/ProjectDetailPage/index.ts | 1 + .../ProjectsPage/ProjectsPage.tsx | 11 +++++------ .../{ => projectViews}/ProjectsPage/index.ts | 0 .../{ => projectViews}/ProjectsPage/styles.ts | 0 frontend/src/views/projectViews/index.ts | 2 ++ 12 files changed, 40 insertions(+), 14 deletions(-) delete mode 100644 frontend/src/views/ProjectsPage/ProjectsPage.css create mode 100644 frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx create mode 100644 frontend/src/views/projectViews/ProjectDetailPage/index.ts rename frontend/src/views/{ => projectViews}/ProjectsPage/ProjectsPage.tsx (91%) rename frontend/src/views/{ => projectViews}/ProjectsPage/index.ts (100%) rename frontend/src/views/{ => projectViews}/ProjectsPage/styles.ts (100%) create mode 100644 frontend/src/views/projectViews/index.ts diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 2fcc3c433..eec3fd9ef 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -7,7 +7,7 @@ import { BrowserRouter, Navigate, Outlet, Route, Routes } from "react-router-dom import RegisterPage from "./views/RegisterPage"; import StudentsPage from "./views/StudentsPage/StudentsPage"; import UsersPage from "./views/UsersPage"; -import ProjectsPage from "./views/ProjectsPage/ProjectsPage"; +import { ProjectsPage, ProjectDetailPage } from "./views"; import PendingPage from "./views/PendingPage"; import Footer from "./components/Footer"; import { useAuth } from "./contexts/auth-context"; @@ -63,7 +63,7 @@ export default function Router() { } /> {/* TODO project page */} - } /> + } /> {/* Students routes */} diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 828143795..7c0b47910 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -29,7 +29,7 @@ export default function ProjectCard({ numberOfStudents, coaches, edition, - id, + projectId, refreshEditions, }: { name: string; @@ -37,13 +37,13 @@ export default function ProjectCard({ numberOfStudents: number; coaches: Coach[]; edition: string; - id: string; + projectId: number; refreshEditions: () => void; }) { const [show, setShow] = useState(false); const handleClose = () => setShow(false); const handleDelete = () => { - deleteProject(edition, id); + deleteProject(edition, projectId); setShow(false); refreshEditions(); }; diff --git a/frontend/src/data/interfaces/projects.ts b/frontend/src/data/interfaces/projects.ts index 869161649..5642b3e95 100644 --- a/frontend/src/data/interfaces/projects.ts +++ b/frontend/src/data/interfaces/projects.ts @@ -13,5 +13,5 @@ export interface Project { partners: Partner[]; coaches: Coach[]; editionName: string; - projectId: string; + projectId: number; } \ No newline at end of file diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index 139398ab1..002d3b892 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -15,7 +15,22 @@ export async function getProjects(edition: string) { } } -export async function deleteProject(edition: string, projectId: string) { +export async function getProject(edition:string, projectId: number) { + try { + const response = await axiosInstance.get("/editions/" + edition + "/projects/" + projectId); + const project = response.data; + return project; + } catch (error) { + if (axios.isAxiosError(error)) { + return false; + } else { + throw error; + } + } + +} + +export async function deleteProject(edition: string, projectId: number) { try { const response = await axiosInstance.delete( "/editions/" + edition + "/projects/" + projectId diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.css b/frontend/src/views/ProjectsPage/ProjectsPage.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/src/views/index.ts b/frontend/src/views/index.ts index 72c6efdc9..137777e61 100644 --- a/frontend/src/views/index.ts +++ b/frontend/src/views/index.ts @@ -1,7 +1,8 @@ export * as Errors from "./errors"; export { default as LoginPage } from "./LoginPage"; export { default as PendingPage } from "./PendingPage"; -export { default as ProjectsPage } from "./ProjectsPage"; +export { ProjectsPage } from "./projectViews"; +export { ProjectDetailPage } from "./projectViews" export { default as RegisterPage } from "./RegisterPage"; export { default as StudentsPage } from "./StudentsPage"; export { default as UsersPage } from "./UsersPage"; diff --git a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx new file mode 100644 index 000000000..14f45fb21 --- /dev/null +++ b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx @@ -0,0 +1,8 @@ +import { useParams } from "react-router-dom"; + +export default function ProjectDetailPage() { + const params = useParams(); + const projectId = params.projectId; + + return
                                        {projectId}
                                        ; +} diff --git a/frontend/src/views/projectViews/ProjectDetailPage/index.ts b/frontend/src/views/projectViews/ProjectDetailPage/index.ts new file mode 100644 index 000000000..445f2f4aa --- /dev/null +++ b/frontend/src/views/projectViews/ProjectDetailPage/index.ts @@ -0,0 +1 @@ +export { default } from "./ProjectDetailPage"; diff --git a/frontend/src/views/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx similarity index 91% rename from frontend/src/views/ProjectsPage/ProjectsPage.tsx rename to frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index d49345785..d3837856b 100644 --- a/frontend/src/views/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -1,10 +1,9 @@ import { useEffect, useState } from "react"; -import { getProjects } from "../../utils/api/projects"; -import "./ProjectsPage.css"; -import { ProjectCard } from "../../components/ProjectsComponents"; +import { getProjects } from "../../../utils/api/projects"; +import { ProjectCard } from "../../../components/ProjectsComponents"; import { CardsGrid, CreateButton, SearchButton, SearchField, OwnProject } from "./styles"; -import { useAuth } from "../../contexts/auth-context"; -import { Project } from "../../data/interfaces"; +import { useAuth } from "../../../contexts/auth-context"; +import { Project } from "../../../data/interfaces"; function ProjectPage() { const [projectsAPI, setProjectsAPI] = useState>([]); @@ -87,7 +86,7 @@ function ProjectPage() { numberOfStudents={project.numberOfStudents} coaches={project.coaches} edition={project.editionName} - id={project.projectId} + projectId={project.projectId} refreshEditions={() => setGotProjects(false)} key={_index} /> diff --git a/frontend/src/views/ProjectsPage/index.ts b/frontend/src/views/projectViews/ProjectsPage/index.ts similarity index 100% rename from frontend/src/views/ProjectsPage/index.ts rename to frontend/src/views/projectViews/ProjectsPage/index.ts diff --git a/frontend/src/views/ProjectsPage/styles.ts b/frontend/src/views/projectViews/ProjectsPage/styles.ts similarity index 100% rename from frontend/src/views/ProjectsPage/styles.ts rename to frontend/src/views/projectViews/ProjectsPage/styles.ts diff --git a/frontend/src/views/projectViews/index.ts b/frontend/src/views/projectViews/index.ts new file mode 100644 index 000000000..944f237c5 --- /dev/null +++ b/frontend/src/views/projectViews/index.ts @@ -0,0 +1,2 @@ +export { default as ProjectsPage } from "./ProjectsPage"; +export { default as ProjectDetailPage } from "./ProjectDetailPage" \ No newline at end of file From 9fd4fa8e40498e00e79c31a2e934ba1da879da90 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 8 Apr 2022 21:25:55 +0200 Subject: [PATCH 312/536] change coupple of suggestions --- backend/src/app/logic/students.py | 4 +- .../app/routers/editions/students/students.py | 13 ++- .../test_students/test_students.py | 80 ++++--------------- 3 files changed, 24 insertions(+), 73 deletions(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index de3fd770b..9e2208845 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -23,8 +23,8 @@ def get_students_search(db: Session, edition: Edition, commons: CommonQueryParam if commons.skill_ids: skills: list[Skill] = db.query(Skill).where( Skill.skill_id.in_(commons.skill_ids)).all() - if not skills: #TODO: should this be a costum error with a message or not? - raise NoResultFound + if len(skills) != len(commons.skill_ids): + return ReturnStudentList(students=[]) else: skills = [] students = get_students(db, edition, first_name=commons.first_name, diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index 36bd8db0b..754788b4f 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -15,17 +15,16 @@ students_suggestions_router, prefix="/{student_id}") -@students_router.get("/", dependencies=[Depends(require_auth)]) +@students_router.get("/", dependencies=[Depends(require_auth)], response_model=ReturnStudentList) async def get_students(db: Session = Depends(get_session), commons: CommonQueryParams = Depends(CommonQueryParams), - edition: Edition = Depends(get_edition)) -> ReturnStudentList: + edition: Edition = Depends(get_edition)): """ Get a list of all students. """ return get_students_search(db, edition, commons) - @students_router.post("/emails") async def send_emails(edition: Edition = Depends(get_edition)): """ @@ -34,15 +33,15 @@ async def send_emails(edition: Edition = Depends(get_edition)): @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)) -> None: +async def delete_student(student: Student = Depends(get_student), db: Session = Depends(get_session)): """ Delete all information stored about a specific student. """ remove_student(db, student) -@students_router.get("/{student_id}", dependencies=[Depends(require_auth)]) -async def get_student_by_id(edition: Edition = Depends(get_edition), student: Student = Depends(get_student)) -> ReturnStudent: +@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)): """ Get information about a specific student. """ @@ -50,7 +49,7 @@ async def get_student_by_id(edition: Edition = Depends(get_edition), student: St @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)) -> None: +async def make_decision(decision: NewDecision, student: Student = Depends(get_student), db: Session = Depends(get_session)): """ Make a finalized Yes/Maybe/No decision about a student. 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 212fd613a..91775e4a5 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 @@ -17,28 +17,6 @@ def database_with_data(database_session: Session) -> Session: database_session.add(edition) database_session.commit() - # Users - admin: User = User(name="admin", admin=True, editions=[edition]) - coach1: User = User(name="coach1", editions=[edition]) - coach2: User = User(name="coach2", editions=[edition]) - database_session.add(admin) - database_session.add(coach1) - database_session.add(coach2) - database_session.commit() - - # AuthEmail - pw_hash = get_password_hash("wachtwoord") - auth_email_admin: AuthEmail = AuthEmail( - user=admin, email="admin@ngmail.com", pw_hash=pw_hash) - auth_email_coach1: AuthEmail = AuthEmail( - user=coach1, email="coach1@noutlook.be", pw_hash=pw_hash) - auth_email_coach2: AuthEmail = AuthEmail( - user=coach2, email="coach2@noutlook.be", pw_hash=pw_hash) - database_session.add(auth_email_admin) - database_session.add(auth_email_coach1) - database_session.add(auth_email_coach2) - database_session.commit() - # Skill skill1: Skill = Skill(name="skill1", description="something about skill1") skill2: Skill = Skill(name="skill2", description="something about skill2") @@ -68,52 +46,13 @@ def database_with_data(database_session: Session) -> Session: return database_session -@pytest.fixture -def auth_coach1(auth_client: AuthClient) -> str: - """A fixture for logging in coach1""" - - form = { - "username": "coach1@noutlook.be", - "password": "wachtwoord" - } - token = auth_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token - return auth - - -@pytest.fixture -def auth_coach2(auth_client: AuthClient) -> str: - """A fixture for logging in coach1""" - - form = { - "username": "coach2@noutlook.be", - "password": "wachtwoord" - } - token = auth_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token - return auth - - -@pytest.fixture -def auth_admin(auth_client: AuthClient) -> str: - """A fixture for logging in admin""" - - form = { - "username": "admin@ngmail.com", - "password": "wachtwoord" - } - token = auth_client.post("/login/token", data=form).json()["accessToken"] - auth = "Bearer " + token - return auth - - def test_set_definitive_decision_no_authorization(database_with_data: Session, auth_client: AuthClient): """tests""" assert auth_client.put( "/editions/ed2022/students/2/decision").status_code == status.HTTP_401_UNAUTHORIZED -def test_set_definitive_decision_coach(database_with_data: Session, auth_client: AuthClient, auth_coach1): +def test_set_definitive_decision_coach(database_with_data: Session, auth_client: AuthClient): """tests""" edition: Edition = database_with_data.query(Edition).all()[0] auth_client.coach(edition) @@ -171,7 +110,7 @@ def test_delete_student_no_authorization(database_with_data: Session, auth_clien "Authorization": "auth"}).status_code == status.HTTP_401_UNAUTHORIZED -def test_delete_student_coach(database_with_data: Session, auth_client: AuthClient, auth_coach1): +def test_delete_student_coach(database_with_data: Session, auth_client: AuthClient): """tests""" edition: Edition = database_with_data.query(Edition).all()[0] auth_client.coach(edition) @@ -303,4 +242,17 @@ def test_get_ghost_skill_students(database_with_data: Session, auth_client: Auth edition: Edition = database_with_data.query(Edition).all()[0] auth_client.coach(edition) response = auth_client.get("/editions/ed2022/students/?skill_ids=100") - assert response.status_code == status.HTTP_404_NOT_FOUND + print(response.json()) + 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): + """tests""" + 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") + print(response.json()) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["students"]) == 0 From a80a6e0f360c8964a8ef27d6c40d6df74c3f183d Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 8 Apr 2022 21:27:22 +0200 Subject: [PATCH 313/536] deleted unused insertions in database in tests --- .../test_suggestions/test_suggestions.py | 17 ----------------- 1 file changed, 17 deletions(-) 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 1bc8c9696..9ee60ede8 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 @@ -18,25 +18,8 @@ def database_with_data(database_session: Session) -> Session: database_session.commit() # Users - admin: User = User(name="admin", admin=True, editions=[edition]) coach1: User = User(name="coach1", editions=[edition]) - coach2: User = User(name="coach2", editions=[edition]) - database_session.add(admin) database_session.add(coach1) - database_session.add(coach2) - database_session.commit() - - # AuthEmail - pw_hash = get_password_hash("wachtwoord") - auth_email_admin: AuthEmail = AuthEmail( - user=admin, email="admin@ngmail.com", pw_hash=pw_hash) - auth_email_coach1: AuthEmail = AuthEmail( - user=coach1, email="coach1@noutlook.be", pw_hash=pw_hash) - auth_email_coach2: AuthEmail = AuthEmail( - user=coach2, email="coach2@noutlook.be", pw_hash=pw_hash) - database_session.add(auth_email_admin) - database_session.add(auth_email_coach1) - database_session.add(auth_email_coach2) database_session.commit() # Skill From 0f8a5ec4069418ca236a663d577cdbb21447d959 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 8 Apr 2022 21:38:16 +0200 Subject: [PATCH 314/536] Fix typo in imports --- frontend/src/components/Navbar/Navbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Navbar/Navbar.tsx b/frontend/src/components/Navbar/Navbar.tsx index e49aa6804..75b736178 100644 --- a/frontend/src/components/Navbar/Navbar.tsx +++ b/frontend/src/components/Navbar/Navbar.tsx @@ -1,4 +1,4 @@ -import Container from "react-bootstrap/esm/Container"; +import Container from "react-bootstrap/Container"; import { BSNavbar, StyledDropdownItem } from "./styles"; import { useAuth } from "../../contexts"; import Brand from "./Brand"; From 940d520da259216abdecad669e283a9af3e08cc4 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Fri, 8 Apr 2022 21:46:26 +0200 Subject: [PATCH 315/536] get project data --- .../ProjectDetailPage/ProjectDetailPage.tsx | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx index 14f45fb21..9628e9a72 100644 --- a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx +++ b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx @@ -1,8 +1,33 @@ -import { useParams } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { Project } from "../../../data/interfaces"; + +import { getProject } from "../../../utils/api/projects"; export default function ProjectDetailPage() { const params = useParams(); - const projectId = params.projectId; + const projectId = parseInt(params.projectId!); + + const [project, setProject] = useState(); + const [gotProject, setGotProject] = useState(false); + + const navigate = useNavigate(); - return
                                        {projectId}
                                        ; + useEffect(() => { + async function callProjects() { + if (projectId) { + setGotProject(true); + const response = await getProject("summerof2022", projectId); + if (response) { + setProject(response); + } else navigate("/404-not-found"); + } + } + if (!gotProject) { + callProjects(); + } + }); + if (project) { + return
                                        {project.name}
                                        ; + } else return
                                        ; } From 98cfaef29a9e4fe9d5a1fea4dc93e93f4b884e9b Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 8 Apr 2022 22:14:02 +0200 Subject: [PATCH 316/536] more changes --- backend/src/app/logic/students.py | 6 ++---- backend/src/app/schemas/students.py | 3 +-- backend/src/database/crud/skills.py | 5 +++++ .../test_editions/test_students/test_students.py | 3 +-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index 9e2208845..a6c903486 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -1,8 +1,8 @@ from sqlalchemy.orm import Session -from sqlalchemy.orm.exc import NoResultFound from src.app.schemas.students import NewDecision from src.database.crud.students import set_definitive_decision_on_student, delete_student, get_students +from src.database.crud.skills import get_skills_by_ids from src.database.models import Edition, Student, Skill from src.app.schemas.students import ReturnStudentList, ReturnStudent, CommonQueryParams @@ -19,10 +19,8 @@ def remove_student(db: Session, student: Student) -> None: def get_students_search(db: Session, edition: Edition, commons: CommonQueryParams) -> ReturnStudentList: """return all students""" - # TODO: use function in crud/skills.py if commons.skill_ids: - skills: list[Skill] = db.query(Skill).where( - Skill.skill_id.in_(commons.skill_ids)).all() + skills: list[Skill] = get_skills_by_ids(db, commons.skill_ids) if len(skills) != len(commons.skill_ids): return ReturnStudentList(students=[]) else: diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index d21cc4052..33dd4c489 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -11,7 +11,7 @@ class NewDecision(CamelCaseModel): class Student(CamelCaseModel): """ - Model to represent a Coach + Model to represent a Student Sent as a response to API /GET requests """ student_id: int @@ -21,7 +21,6 @@ class Student(CamelCaseModel): email_address: str phone_number: str alumni: bool - # cv_url = Column(Text) decision: DecisionEnum wants_to_be_student_coach: bool edition_id: int diff --git a/backend/src/database/crud/skills.py b/backend/src/database/crud/skills.py index f1592f4e2..194e60693 100644 --- a/backend/src/database/crud/skills.py +++ b/backend/src/database/crud/skills.py @@ -15,6 +15,11 @@ def get_skills(db: Session) -> list[Skill]: return db.query(Skill).all() +def get_skills_by_ids(db: Session, skill_ids) -> list[Skill]: + """Get all skills from list of skill ids""" + return db.query(Skill).where(Skill.skill_id.in_(skill_ids)).all() + + def create_skill(db: Session, skill: SkillBase) -> Skill: """Add a new skill into the database. 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 91775e4a5..31d44326e 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 @@ -2,8 +2,7 @@ from sqlalchemy.orm import Session from starlette import status from src.database.enums import DecisionEnum -from src.database.models import Student, User, Edition, Skill, AuthEmail -from src.app.logic.security import get_password_hash +from src.database.models import Student, Edition, Skill from tests.utils.authorization import AuthClient From 6514c8c66fbfc4a6800db8300f8fd7a294f990be Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Fri, 8 Apr 2022 22:22:35 +0200 Subject: [PATCH 317/536] when clicking a project title it takes you to that project details page --- .../ProjectsComponents/ProjectCard/ProjectCard.tsx | 9 ++++++++- .../ProjectsComponents/ProjectCard/styles.ts | 12 ++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 7c0b47910..820668052 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -7,6 +7,7 @@ import { Delete, TitleContainer, Title, + OpenIcon, ClientContainer, Client, } from "./styles"; @@ -18,6 +19,7 @@ import { useState } from "react"; import ConfirmDelete from "../ConfirmDelete"; import { deleteProject } from "../../../utils/api/projects"; +import { useNavigate } from "react-router-dom"; interface Coach { name: string; @@ -49,10 +51,15 @@ export default function ProjectCard({ }; const handleShow = () => setShow(true); + const navigate = useNavigate(); + return ( - {name} + navigate("/editions/summerof2022/projects/" + projectId)}> + {name} + <OpenIcon/> + diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts index eae0656d7..7e81204a6 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts +++ b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts @@ -1,5 +1,7 @@ import { Modal } from "react-bootstrap"; import styled from "styled-components"; +import { BsArrowUpRightSquare } from "react-icons/bs"; + export const CardContainer = styled.div` border: 2px solid #1a1a36; @@ -19,8 +21,18 @@ export const TitleContainer = styled.div` export const Title = styled.h2` text-overflow: ellipsis; overflow: hidden; + display: flex; + align-items: center; + :hover { + cursor: pointer; + } `; +export const OpenIcon = styled(BsArrowUpRightSquare)` + margin-left: 10px; + height: 20px; +` + export const ClientContainer = styled.div` display: flex; align-items: top; From 2d407d9516b9c986981a2d2faa98d783860309c8 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 8 Apr 2022 22:23:36 +0200 Subject: [PATCH 318/536] decent docstrings for tests added --- .../test_database/test_crud/test_students.py | 42 ++++++++------ .../test_students/test_students.py | 55 ++++++++++--------- 2 files changed, 54 insertions(+), 43 deletions(-) diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index f17abd4a4..1f0c2441d 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -3,7 +3,8 @@ from sqlalchemy.orm.exc import NoResultFound from src.database.models import Student, User, Edition, Skill from src.database.enums import DecisionEnum -from src.database.crud.students import get_student_by_id, set_definitive_decision_on_student, delete_student, get_students +from src.database.crud.students import (get_student_by_id, set_definitive_decision_on_student, + delete_student, get_students) @pytest.fixture @@ -101,51 +102,60 @@ def test_delete_student(database_with_data: Session): def test_get_all_students(database_with_data: Session): - """test""" - edition: Edition = database_with_data.query(Edition).where(Edition.edition_id == 1).one() + """test get all students""" + edition: Edition = database_with_data.query( + Edition).where(Edition.edition_id == 1).one() students = get_students(database_with_data, edition) assert len(students) == 2 def test_search_students_on_first_name(database_with_data: Session): """test""" - edition: Edition = database_with_data.query(Edition).where(Edition.edition_id == 1).one() + edition: Edition = database_with_data.query( + Edition).where(Edition.edition_id == 1).one() students = get_students(database_with_data, edition, first_name="Jos") assert len(students) == 1 def test_search_students_on_last_name(database_with_data: Session): - """tests""" - edition: Edition = database_with_data.query(Edition).where(Edition.edition_id == 1).one() + """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, last_name="Vermeulen") assert len(students) == 1 def test_search_students_alumni(database_with_data: Session): - """tests""" - edition: Edition = database_with_data.query(Edition).where(Edition.edition_id == 1).one() + """tests search on alumni""" + edition: Edition = database_with_data.query( + Edition).where(Edition.edition_id == 1).one() students = get_students(database_with_data, edition, alumni=True) assert len(students) == 1 def test_search_students_student_coach(database_with_data: Session): - """tests""" - edition: Edition = database_with_data.query(Edition).where(Edition.edition_id == 1).one() + """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, student_coach=True) assert len(students) == 1 def test_search_students_one_skill(database_with_data: Session): - """tests""" - 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() + """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, skills=[skill]) assert len(students) == 1 def test_search_students_multiple_skills(database_with_data: Session): - """tests""" - 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() + """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, skills=skills) assert len(students) == 1 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 31d44326e..9327c1d4f 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 @@ -46,13 +46,13 @@ def database_with_data(database_session: Session) -> Session: def test_set_definitive_decision_no_authorization(database_with_data: Session, auth_client: AuthClient): - """tests""" + """tests that you have to be logged in""" assert auth_client.put( "/editions/ed2022/students/2/decision").status_code == status.HTTP_401_UNAUTHORIZED def test_set_definitive_decision_coach(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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( @@ -60,21 +60,21 @@ def test_set_definitive_decision_coach(database_with_data: Session, auth_client: def test_set_definitive_decision_on_ghost(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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 def test_set_definitive_decision_wrong_body(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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 def test_set_definitive_decision_yes(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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 @@ -84,7 +84,7 @@ def test_set_definitive_decision_yes(database_with_data: Session, auth_client: A def test_set_definitive_decision_no(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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 @@ -94,7 +94,7 @@ def test_set_definitive_decision_no(database_with_data: Session, auth_client: Au def test_set_definitive_decision_maybe(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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 @@ -104,13 +104,13 @@ def test_set_definitive_decision_maybe(database_with_data: Session, auth_client: def test_delete_student_no_authorization(database_with_data: Session, auth_client: AuthClient): - """tests""" - assert auth_client.delete("/editions/ed2022/students/2", headers={ - "Authorization": "auth"}).status_code == status.HTTP_401_UNAUTHORIZED + """tests that you have to be logged in""" + assert auth_client.delete( + "/editions/ed2022/students/2").status_code == status.HTTP_401_UNAUTHORIZED def test_delete_student_coach(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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( @@ -121,7 +121,7 @@ def test_delete_student_coach(database_with_data: Session, auth_client: AuthClie def test_delete_ghost(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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 @@ -131,7 +131,7 @@ def test_delete_ghost(database_with_data: Session, auth_client: AuthClient): def test_delete(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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 @@ -141,26 +141,27 @@ def test_delete(database_with_data: Session, auth_client: AuthClient): def test_get_student_by_id_no_autorization(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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 def test_get_student_by_id(database_with_data: Session, auth_client: AuthClient): - """tests""" - auth_client.admin() + """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 def test_get_students_no_autorization(database_with_data: Session, auth_client: AuthClient): - """tests""" + """tests you have to be logged in to get all students""" assert auth_client.get( "/editions/ed2022/students/").status_code == status.HTTP_401_UNAUTHORIZED def test_get_all_students(database_with_data: Session, auth_client: AuthClient): - """tests""" + """tests get all students""" edition: Edition = database_with_data.query(Edition).all()[0] auth_client.coach(edition) response = auth_client.get("/editions/ed2022/students/") @@ -169,7 +170,7 @@ def test_get_all_students(database_with_data: Session, auth_client: AuthClient): def test_get_first_name_students(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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/?first_name=Jos") @@ -178,7 +179,7 @@ def test_get_first_name_students(database_with_data: Session, auth_client: AuthC def test_get_last_name_students(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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( @@ -188,7 +189,7 @@ def test_get_last_name_students(database_with_data: Session, auth_client: AuthCl def test_get_alumni_students(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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") @@ -197,7 +198,7 @@ def test_get_alumni_students(database_with_data: Session, auth_client: AuthClien def test_get_student_coach_students(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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") @@ -206,7 +207,7 @@ def test_get_student_coach_students(database_with_data: Session, auth_client: Au def test_get_one_skill_students(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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") @@ -216,7 +217,7 @@ def test_get_one_skill_students(database_with_data: Session, auth_client: AuthCl def test_get_multiple_skill_students(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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( @@ -227,7 +228,7 @@ def test_get_multiple_skill_students(database_with_data: Session, auth_client: A def test_get_multiple_skill_students_no_students(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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( @@ -237,7 +238,7 @@ def test_get_multiple_skill_students_no_students(database_with_data: Session, au def test_get_ghost_skill_students(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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") @@ -247,7 +248,7 @@ def test_get_ghost_skill_students(database_with_data: Session, auth_client: Auth def test_get_one_real_one_ghost_skill_students(database_with_data: Session, auth_client: AuthClient): - """tests""" + """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( From f10d9ffe0a633d3fae183f95e4bad6d23124d4aa Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 8 Apr 2022 22:24:29 +0200 Subject: [PATCH 319/536] Refactor imports --- frontend/src/Router.tsx | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 2fcc3c433..368bfaaea 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -1,20 +1,18 @@ import React from "react"; -import VerifyingTokenPage from "./views/VerifyingTokenPage"; -import LoginPage from "./views/LoginPage"; import { Container, ContentWrapper } from "./app.styles"; -import NavBar from "./components/navbar"; import { BrowserRouter, Navigate, Outlet, Route, Routes } from "react-router-dom"; -import RegisterPage from "./views/RegisterPage"; -import StudentsPage from "./views/StudentsPage/StudentsPage"; -import UsersPage from "./views/UsersPage"; -import ProjectsPage from "./views/ProjectsPage/ProjectsPage"; -import PendingPage from "./views/PendingPage"; -import Footer from "./components/Footer"; +import { AdminRoute, Footer, NavBar, PrivateRoute } from "./components"; import { useAuth } from "./contexts/auth-context"; -import PrivateRoute from "./components/PrivateRoute"; -import AdminRoute from "./components/AdminRoute"; -import { NotFoundPage } from "./views/errors"; -import ForbiddenPage from "./views/errors/ForbiddenPage"; +import { + LoginPage, + PendingPage, + ProjectsPage, + RegisterPage, + StudentsPage, + UsersPage, + VerifyingTokenPage, +} from "./views"; +import { ForbiddenPage, NotFoundPage } from "./views/errors"; /** * Router component to render different pages depending on the current url. Renders From 594815ab8ad6058c1e3c63e4b060771fdabc7951 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 8 Apr 2022 22:27:33 +0200 Subject: [PATCH 320/536] Spell osoc properly --- frontend/public/index.html | 4 ++-- frontend/public/manifest.json | 2 +- frontend/src/components/Navbar/Brand.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/public/index.html b/frontend/public/index.html index b2edc6981..94b643f6c 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -7,7 +7,7 @@ - Open Summer Of Code + Open Summer of Code diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json index 7c09c69b7..480e85c03 100644 --- a/frontend/public/manifest.json +++ b/frontend/public/manifest.json @@ -1,6 +1,6 @@ { "short_name": "OSOC Selections 3", - "name": "Open Summer Of Code Selections Tool - Team 3", + "name": "Open Summer of Code Selections Tool - Team 3", "icons": [ { "src": "favicon.ico", diff --git a/frontend/src/components/Navbar/Brand.tsx b/frontend/src/components/Navbar/Brand.tsx index 4e7e79bc9..2a863d493 100644 --- a/frontend/src/components/Navbar/Brand.tsx +++ b/frontend/src/components/Navbar/Brand.tsx @@ -13,7 +13,7 @@ export default function Brand() { alt={"OSOC logo (light)"} className={"me-2"} />{" "} - Open Summer Of Code + Open Summer of Code
                                        ); } From 7710967cc449cf2d12103e21d14e0a1df75f60d9 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 8 Apr 2022 22:35:36 +0200 Subject: [PATCH 321/536] try to add skills to students in return type --- backend/src/app/schemas/students.py | 3 +++ .../test_routers/test_editions/test_students/test_students.py | 1 + 2 files changed, 4 insertions(+) diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index 33dd4c489..c9304f080 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -2,6 +2,7 @@ from src.app.schemas.webhooks import CamelCaseModel from src.database.enums import DecisionEnum +from src.app.schemas.skills import SkillList class NewDecision(CamelCaseModel): @@ -25,6 +26,8 @@ class Student(CamelCaseModel): wants_to_be_student_coach: bool edition_id: int + skills: SkillList + class Config: orm_mode = True 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 9327c1d4f..576d9d161 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 @@ -222,6 +222,7 @@ def test_get_multiple_skill_students(database_with_data: Session, auth_client: A auth_client.coach(edition) response = auth_client.get( "/editions/ed2022/students/?skill_ids=4&skill_ids=5") + print(response.json()) assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 1 assert response.json()["students"][0]["firstName"] == "Marta" From 643e5f58ccdaf5eea9a057b1d46a3664ce336a51 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Fri, 8 Apr 2022 22:45:25 +0200 Subject: [PATCH 322/536] go back to overview text added --- .../ProjectsComponents/ProjectCard/styles.ts | 3 ++- .../ProjectDetailPage/ProjectDetailPage.tsx | 17 ++++++++++++++++- .../projectViews/ProjectDetailPage/styles.ts | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 frontend/src/views/projectViews/ProjectDetailPage/styles.ts diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts index 7e81204a6..6c9e23ff4 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts +++ b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts @@ -29,7 +29,8 @@ export const Title = styled.h2` `; export const OpenIcon = styled(BsArrowUpRightSquare)` - margin-left: 10px; + margin-left: 5px; + margin-top: 2px; height: 20px; ` diff --git a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx index 9628e9a72..48f1237e5 100644 --- a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx +++ b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx @@ -3,6 +3,9 @@ import { useNavigate, useParams } from "react-router-dom"; import { Project } from "../../../data/interfaces"; import { getProject } from "../../../utils/api/projects"; +import { GoBack, ProjectContainer } from "./styles"; + +import { BiArrowBack } from "react-icons/bi"; export default function ProjectDetailPage() { const params = useParams(); @@ -28,6 +31,18 @@ export default function ProjectDetailPage() { } }); if (project) { - return
                                        {project.name}
                                        ; + return ( +
                                        + + navigate("/editions/summerof2022/projects/")}> + + Overview + + +

                                        {project.name}

                                        + {project.editionName} +
                                        +
                                        + ); } else return
                                        ; } diff --git a/frontend/src/views/projectViews/ProjectDetailPage/styles.ts b/frontend/src/views/projectViews/ProjectDetailPage/styles.ts new file mode 100644 index 000000000..ffd1be725 --- /dev/null +++ b/frontend/src/views/projectViews/ProjectDetailPage/styles.ts @@ -0,0 +1,15 @@ +import styled from "styled-components"; + +export const ProjectContainer = styled.div` + margin: 20px; +`; + +export const GoBack = styled.div` + display: flex; + align-items: center; + margin-bottom: 5px; + + :hover { + cursor: pointer; + } +`; From 28b2998f2f881bbbc2e12693f3ed00527aad145b Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 8 Apr 2022 22:57:11 +0200 Subject: [PATCH 323/536] added skills to response student --- backend/src/app/schemas/students.py | 4 ++-- .../test_routers/test_editions/test_students/test_students.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index c9304f080..83fed943b 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -2,7 +2,7 @@ from src.app.schemas.webhooks import CamelCaseModel from src.database.enums import DecisionEnum -from src.app.schemas.skills import SkillList +from src.app.schemas.skills import Skill class NewDecision(CamelCaseModel): @@ -26,7 +26,7 @@ class Student(CamelCaseModel): wants_to_be_student_coach: bool edition_id: int - skills: SkillList + skills: list[Skill] class Config: orm_mode = True 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 576d9d161..900c8208c 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 @@ -243,7 +243,6 @@ def test_get_ghost_skill_students(database_with_data: Session, auth_client: Auth edition: Edition = database_with_data.query(Edition).all()[0] auth_client.coach(edition) response = auth_client.get("/editions/ed2022/students/?skill_ids=100") - print(response.json()) assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 0 From 0e6b97c660f820fcdbff21d1f170caea64dd1120 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Fri, 8 Apr 2022 23:55:22 +0200 Subject: [PATCH 324/536] support for multiple partners --- .../ProjectCard/ProjectCard.tsx | 17 ++++++++------ .../ProjectsComponents/ProjectCard/styles.ts | 20 ++++++++--------- .../ProjectDetailPage/ProjectDetailPage.tsx | 22 ++++++++++++++++--- .../projectViews/ProjectDetailPage/styles.ts | 22 +++++++++++++++++++ .../ProjectsPage/ProjectsPage.tsx | 2 +- 5 files changed, 62 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 820668052..c869369fc 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -10,6 +10,7 @@ import { OpenIcon, ClientContainer, Client, + Clients, } from "./styles"; import { BsPersonFill } from "react-icons/bs"; @@ -21,13 +22,11 @@ import ConfirmDelete from "../ConfirmDelete"; import { deleteProject } from "../../../utils/api/projects"; import { useNavigate } from "react-router-dom"; -interface Coach { - name: string; -} +import { Coach, Partner } from "../../../data/interfaces"; export default function ProjectCard({ name, - client, + partners, numberOfStudents, coaches, edition, @@ -35,7 +34,7 @@ export default function ProjectCard({ refreshEditions, }: { name: string; - client: string; + partners: Partner[]; numberOfStudents: number; coaches: Coach[]; edition: string; @@ -58,7 +57,7 @@ export default function ProjectCard({ navigate("/editions/summerof2022/projects/" + projectId)}> {name} - <OpenIcon/> + <OpenIcon /> @@ -74,7 +73,11 @@ export default function ProjectCard({ - {client} + + {partners.map((element, _index) => ( + {element.name} + ))} + {numberOfStudents} diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts index 6c9e23ff4..8ec5a8059 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts +++ b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts @@ -2,7 +2,6 @@ import { Modal } from "react-bootstrap"; import styled from "styled-components"; import { BsArrowUpRightSquare } from "react-icons/bs"; - export const CardContainer = styled.div` border: 2px solid #1a1a36; border-radius: 20px; @@ -32,7 +31,7 @@ export const OpenIcon = styled(BsArrowUpRightSquare)` margin-left: 5px; margin-top: 2px; height: 20px; -` +`; export const ClientContainer = styled.div` display: flex; @@ -41,13 +40,17 @@ export const ClientContainer = styled.div` color: lightgray; `; +export const Clients = styled.div` + display: flex; + overflow-x: scroll; +`; + export const Client = styled.h5` - text-overflow: ellipsis; - overflow: hidden; + margin-right: 10px; `; export const NumberOfStudents = styled.div` - margin-left: 2.5%; + margin-left: 10px; display: flex; align-items: center; margin-bottom: 4px; @@ -73,7 +76,7 @@ export const CoachText = styled.div` overflow: hidden; text-overflow: ellipsis; white-space: nowrap; -` +`; export const Delete = styled.button` background-color: #f14a3b; @@ -86,7 +89,4 @@ export const Delete = styled.button` align-items: center; `; -export const PopUp = styled(Modal)` - -` - +export const PopUp = styled(Modal)``; diff --git a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx index 48f1237e5..b93dec106 100644 --- a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx +++ b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx @@ -3,9 +3,17 @@ import { useNavigate, useParams } from "react-router-dom"; import { Project } from "../../../data/interfaces"; import { getProject } from "../../../utils/api/projects"; -import { GoBack, ProjectContainer } from "./styles"; +import { + GoBack, + ProjectContainer, + Client, + ClientContainer, + NumberOfStudents, + Title, +} from "./styles"; import { BiArrowBack } from "react-icons/bi"; +import { BsPersonFill } from "react-icons/bs"; export default function ProjectDetailPage() { const params = useParams(); @@ -39,8 +47,16 @@ export default function ProjectDetailPage() { Overview -

                                        {project.name}

                                        - {project.editionName} + {project.name} + + {project.partners.map((element, _index) => ( + {element.name} + ))} + + {project.numberOfStudents} + + +
                                        ); diff --git a/frontend/src/views/projectViews/ProjectDetailPage/styles.ts b/frontend/src/views/projectViews/ProjectDetailPage/styles.ts index ffd1be725..ffa7ab91c 100644 --- a/frontend/src/views/projectViews/ProjectDetailPage/styles.ts +++ b/frontend/src/views/projectViews/ProjectDetailPage/styles.ts @@ -13,3 +13,25 @@ export const GoBack = styled.div` cursor: pointer; } `; + +export const Title = styled.h2` + text-overflow: ellipsis; + overflow: hidden; +`; + +export const ClientContainer = styled.div` + display: flex; + align-items: center; + color: lightgray; + overflow-x: scroll; +`; + +export const Client = styled.h5` + margin-right: 1%; +`; + +export const NumberOfStudents = styled.div` + display: flex; + align-items: center; + margin-bottom: 4px; +`; diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index d3837856b..4e42a80cb 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -82,7 +82,7 @@ function ProjectPage() { {projects.map((project, _index) => ( Date: Sat, 9 Apr 2022 01:03:49 +0200 Subject: [PATCH 325/536] boilerplate code to display places in a project --- .../StudentPlaceholder/StudentPlaceholder.tsx | 21 ++++++++++ .../StudentPlaceholder/index.ts | 1 + .../StudentPlaceholder/styles.ts | 17 +++++++++ .../components/ProjectsComponents/index.ts | 1 + frontend/src/data/interfaces/projects.ts | 8 +++- .../ProjectDetailPage/ProjectDetailPage.tsx | 38 +++++++++++++++++++ 6 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/ProjectsComponents/StudentPlaceholder/StudentPlaceholder.tsx create mode 100644 frontend/src/components/ProjectsComponents/StudentPlaceholder/index.ts create mode 100644 frontend/src/components/ProjectsComponents/StudentPlaceholder/styles.ts diff --git a/frontend/src/components/ProjectsComponents/StudentPlaceholder/StudentPlaceholder.tsx b/frontend/src/components/ProjectsComponents/StudentPlaceholder/StudentPlaceholder.tsx new file mode 100644 index 000000000..2bace00cf --- /dev/null +++ b/frontend/src/components/ProjectsComponents/StudentPlaceholder/StudentPlaceholder.tsx @@ -0,0 +1,21 @@ +import { TiDeleteOutline } from "react-icons/ti"; +import { StudentPlace } from "../../../data/interfaces/projects"; +import { StudentPlaceContainer, AddStudent } from "./styles"; + +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 new file mode 100644 index 000000000..628878c9a --- /dev/null +++ b/frontend/src/components/ProjectsComponents/StudentPlaceholder/index.ts @@ -0,0 +1 @@ +export { default } from "./StudentPlaceholder"; diff --git a/frontend/src/components/ProjectsComponents/StudentPlaceholder/styles.ts b/frontend/src/components/ProjectsComponents/StudentPlaceholder/styles.ts new file mode 100644 index 000000000..b632afb21 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/StudentPlaceholder/styles.ts @@ -0,0 +1,17 @@ +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: 20px; + 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 09548c092..821e4083e 100644 --- a/frontend/src/components/ProjectsComponents/index.ts +++ b/frontend/src/components/ProjectsComponents/index.ts @@ -1 +1,2 @@ export { default as ProjectCard } from "./ProjectCard"; +export { default as StudentPlaceholder } from "./StudentPlaceholder"; diff --git a/frontend/src/data/interfaces/projects.ts b/frontend/src/data/interfaces/projects.ts index 5642b3e95..b70dc4d54 100644 --- a/frontend/src/data/interfaces/projects.ts +++ b/frontend/src/data/interfaces/projects.ts @@ -14,4 +14,10 @@ export interface Project { coaches: Coach[]; editionName: string; projectId: number; -} \ No newline at end of file +} + +export interface StudentPlace { + available: boolean; + skill: string; + name: string | undefined; +} diff --git a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx index b93dec106..82394844b 100644 --- a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx +++ b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx @@ -15,6 +15,14 @@ import { import { BiArrowBack } from "react-icons/bi"; import { BsPersonFill } from "react-icons/bs"; +import { StudentPlace } from "../../../data/interfaces/projects"; +import { StudentPlaceholder } from "../../../components/ProjectsComponents"; +import { + CoachContainer, + CoachesContainer, + CoachText, +} from "../../../components/ProjectsComponents/ProjectCard/styles"; + export default function ProjectDetailPage() { const params = useParams(); const projectId = parseInt(params.projectId!); @@ -24,6 +32,8 @@ export default function ProjectDetailPage() { const navigate = useNavigate(); + const [students, setStudents] = useState>([]); + useEffect(() => { async function callProjects() { if (projectId) { @@ -31,6 +41,18 @@ export default function ProjectDetailPage() { const response = await getProject("summerof2022", projectId); if (response) { setProject(response); + + // 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); } else navigate("/404-not-found"); } } @@ -38,6 +60,7 @@ export default function ProjectDetailPage() { callProjects(); } }); + if (project) { return (
                                        @@ -48,6 +71,7 @@ export default function ProjectDetailPage() { {project.name} + {project.partners.map((element, _index) => ( {element.name} @@ -57,6 +81,20 @@ export default function ProjectDetailPage() { + + + {project.coaches.map((element, _index) => ( + + {element.name} + + ))} + + +
                                        + {students.map((element: StudentPlace, _index) => ( + + ))} +
                                        ); From 94c69889a6eb64e256d3ed3447f4e8feddeab641 Mon Sep 17 00:00:00 2001 From: Francis <44001949+FKD13@users.noreply.github.com> Date: Sat, 9 Apr 2022 14:55:36 +0200 Subject: [PATCH 326/536] Update backend/settings.py Co-authored-by: Stijn De Clercq <60451863+stijndcl@users.noreply.github.com> --- backend/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/settings.py b/backend/settings.py index 9daa34dfa..7e5b72a96 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -27,7 +27,7 @@ # Option to change te database used. Default False is Mariadb. DB_USE_SQLITE: bool = env.bool("DB_USE_SQLITE", False) # Option to change the pagination size for all endpoints that have pagination. -DB_PAGE_SIZE: int = env.int("DB_PAGE_SIZE", 100) +DB_PAGE_SIZE: int = env.int("DB_PAGE_SIZE", 25) """JWT token key""" SECRET_KEY: str = env.str("SECRET_KEY", "4d16a9cc83d74144322e893c879b5f639088c15dc1606b11226abbd7e97f5ee5") From c17e9e9ae99fe349f32987bf9eae7461392fe21c Mon Sep 17 00:00:00 2001 From: FKD13 Date: Sat, 9 Apr 2022 15:08:02 +0200 Subject: [PATCH 327/536] fix mistake from merge --- backend/src/app/routers/users/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index 5da39ac70..9062e9221 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -4,7 +4,7 @@ import src.app.logic.users as logic from src.app.routers.tags import Tags from src.app.schemas.login import UserData -from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, User as UserSchema +from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, user_model_to_schema from src.app.utils.dependencies import require_admin, get_current_active_user from src.database.database import get_session from src.database.models import User as UserDB From 71a93bdc1f3c2eed6ab913751a29d6e285df131f Mon Sep 17 00:00:00 2001 From: FKD13 Date: Sat, 9 Apr 2022 15:27:57 +0200 Subject: [PATCH 328/536] remove unused import --- backend/src/app/routers/login/login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/routers/login/login.py b/backend/src/app/routers/login/login.py index de2268156..fe96a3091 100644 --- a/backend/src/app/routers/login/login.py +++ b/backend/src/app/routers/login/login.py @@ -11,7 +11,7 @@ from src.app.logic.security import authenticate_user, create_access_token from src.app.logic.users import get_user_editions from src.app.routers.tags import Tags -from src.app.schemas.login import Token, UserData +from src.app.schemas.login import Token from src.app.schemas.users import user_model_to_schema from src.database.database import get_session From b5a85c3e65106c82791fed9f28139ffb8c21c37b Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Sat, 9 Apr 2022 16:17:57 +0200 Subject: [PATCH 329/536] fix pytlint errors --- backend/src/app/logic/suggestions.py | 3 ++- .../editions/students/suggestions/suggestions.py | 7 ++++--- backend/src/app/schemas/suggestion.py | 1 + backend/src/database/crud/suggestions.py | 13 ++++++++++--- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py index 321683770..ca80522ea 100644 --- a/backend/src/app/logic/suggestions.py +++ b/backend/src/app/logic/suggestions.py @@ -1,7 +1,8 @@ from sqlalchemy.orm import Session from src.app.schemas.suggestion import NewSuggestion -from src.database.crud.suggestions import create_suggestion, get_suggestions_of_student, delete_suggestion, update_suggestion +from src.database.crud.suggestions import ( + create_suggestion, get_suggestions_of_student, delete_suggestion, update_suggestion) from src.database.models import Suggestion, User from src.app.schemas.suggestion import SuggestionListResponse, SuggestionResponse, suggestion_model_to_schema from src.app.exceptions.authentication import MissingPermissionsException diff --git a/backend/src/app/routers/editions/students/suggestions/suggestions.py b/backend/src/app/routers/editions/students/suggestions/suggestions.py index 022216009..16221aaff 100644 --- a/backend/src/app/routers/editions/students/suggestions/suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/suggestions.py @@ -7,7 +7,7 @@ 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, - remove_suggestion, change_suggestion) + remove_suggestion, change_suggestion) from src.app.schemas.suggestion import NewSuggestion, SuggestionListResponse, SuggestionResponse @@ -35,14 +35,15 @@ async def delete_suggestion(db: Session = Depends(get_session), user: User = Dep @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)): + user: User = Depends(require_auth), suggestion: Suggestion = Depends(get_suggestion)): """ Edit a suggestion you made about a student. """ change_suggestion(db, new_suggestion, suggestion, user) -@students_suggestions_router.get("/", dependencies=[Depends(require_auth)], status_code=status.HTTP_200_OK, response_model=SuggestionListResponse) +@students_suggestions_router.get("/", dependencies=[Depends(require_auth)], + status_code=status.HTTP_200_OK, response_model=SuggestionListResponse) async def get_suggestions(student: Student = Depends(get_student), db: Session = Depends(get_session)): """ Get all suggestions of a student. diff --git a/backend/src/app/schemas/suggestion.py b/backend/src/app/schemas/suggestion.py index 04745b6d0..db5b30eec 100644 --- a/backend/src/app/schemas/suggestion.py +++ b/backend/src/app/schemas/suggestion.py @@ -22,6 +22,7 @@ class Suggestion(CamelCaseModel): argumentation: str class Config: + """Set to ORM mode""" orm_mode = True diff --git a/backend/src/database/crud/suggestions.py b/backend/src/database/crud/suggestions.py index 286092bb2..5c467ec15 100644 --- a/backend/src/database/crud/suggestions.py +++ b/backend/src/database/crud/suggestions.py @@ -3,27 +3,34 @@ from src.database.models import Suggestion from src.database.enums import DecisionEnum -def create_suggestion(db: Session, user_id: int, student_id: int, decision: DecisionEnum, argumentation: str) -> Suggestion: + +def create_suggestion(db: Session, user_id: int, student_id: int, + 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) + suggestion: Suggestion = Suggestion( + student_id=student_id, coach_id=user_id, suggestion=decision, argumentation=argumentation) db.add(suggestion) db.commit() return suggestion + def get_suggestions_of_student(db: Session, student_id: int) -> list[Suggestion]: """Give all suggestions of a student""" return db.query(Suggestion).where(Suggestion.student_id == student_id).all() -def get_suggestion_by_id(db: Session, suggestion_id:int) -> Suggestion: + +def get_suggestion_by_id(db: Session, suggestion_id: int) -> Suggestion: """Give a suggestion based on the ID""" return db.query(Suggestion).where(Suggestion.suggestion_id == suggestion_id).one() + def delete_suggestion(db: Session, suggestion: Suggestion) -> None: """Delete a suggestion from the database""" db.delete(suggestion) + def update_suggestion(db: Session, suggestion: Suggestion, decision: DecisionEnum, argumentation: str) -> None: """Update a suggestion""" suggestion.suggestion = decision From 99a97931c1934c2e91ae498f410db0a713dcc59e Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Sat, 9 Apr 2022 16:26:41 +0200 Subject: [PATCH 330/536] fixes typing erros of /students --- backend/src/app/logic/suggestions.py | 4 ++-- backend/src/database/crud/suggestions.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py index ca80522ea..755baa156 100644 --- a/backend/src/app/logic/suggestions.py +++ b/backend/src/app/logic/suggestions.py @@ -8,7 +8,7 @@ from src.app.exceptions.authentication import MissingPermissionsException -def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, user: User, student_id: int) -> SuggestionResponse: +def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, user: User, student_id: int | None) -> SuggestionResponse: """"Make a new suggestion""" suggestion_orm = create_suggestion( db, user.user_id, student_id, new_suggestion.suggestion, new_suggestion.argumentation) @@ -16,7 +16,7 @@ def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, user: User, return SuggestionResponse(suggestion=suggestion) -def all_suggestions_of_student(db: Session, student_id: int) -> SuggestionListResponse: +def all_suggestions_of_student(db: Session, student_id: int | None) -> SuggestionListResponse: """Get all suggestions of a student""" suggestions_orm = get_suggestions_of_student(db, student_id) all_suggestions = [] diff --git a/backend/src/database/crud/suggestions.py b/backend/src/database/crud/suggestions.py index 5c467ec15..279e7b555 100644 --- a/backend/src/database/crud/suggestions.py +++ b/backend/src/database/crud/suggestions.py @@ -4,7 +4,7 @@ from src.database.enums import DecisionEnum -def create_suggestion(db: Session, user_id: int, student_id: int, +def create_suggestion(db: Session, user_id: int | None, student_id: int | None, decision: DecisionEnum, argumentation: str) -> Suggestion: """ Create a new suggestion in the database @@ -16,7 +16,7 @@ def create_suggestion(db: Session, user_id: int, student_id: int, return suggestion -def get_suggestions_of_student(db: Session, student_id: int) -> list[Suggestion]: +def get_suggestions_of_student(db: Session, student_id: int | None) -> list[Suggestion]: """Give all suggestions of a student""" return db.query(Suggestion).where(Suggestion.student_id == student_id).all() From c45b4d86584f579f6c712c99fa4392270c373c51 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Sat, 9 Apr 2022 16:33:39 +0200 Subject: [PATCH 331/536] some more pylint erros fixed --- backend/src/app/logic/suggestions.py | 3 ++- .../app/routers/editions/students/suggestions/suggestions.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py index 755baa156..69c9a61ae 100644 --- a/backend/src/app/logic/suggestions.py +++ b/backend/src/app/logic/suggestions.py @@ -8,7 +8,8 @@ from src.app.exceptions.authentication import MissingPermissionsException -def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, user: User, student_id: int | None) -> SuggestionResponse: +def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, + user: User, student_id: int | None) -> SuggestionResponse: """"Make a new suggestion""" suggestion_orm = create_suggestion( db, user.user_id, student_id, new_suggestion.suggestion, new_suggestion.argumentation) diff --git a/backend/src/app/routers/editions/students/suggestions/suggestions.py b/backend/src/app/routers/editions/students/suggestions/suggestions.py index 16221aaff..113b5aefb 100644 --- a/backend/src/app/routers/editions/students/suggestions/suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/suggestions.py @@ -33,7 +33,8 @@ async def delete_suggestion(db: Session = Depends(get_session), user: User = Dep remove_suggestion(db, suggestion, user) -@students_suggestions_router.put("/{suggestion_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_student)]) +@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)): """ From 371b35ef97f8490376d797ef96b268b21845ba10 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Sat, 9 Apr 2022 17:23:26 +0200 Subject: [PATCH 332/536] remove editionName & rename functions --- backend/src/app/logic/invites.py | 15 ++---- backend/src/app/logic/projects.py | 54 ++++++------------- backend/src/app/logic/projects_students.py | 24 ++++----- backend/src/app/logic/users.py | 14 ++--- .../app/routers/editions/invites/invites.py | 5 +- .../app/routers/editions/projects/projects.py | 19 +++---- .../projects/students/projects_students.py | 11 ++-- .../suggestions/students_suggestions.py | 2 +- backend/src/app/schemas/invites.py | 1 - backend/src/app/schemas/projects.py | 14 ++++- backend/src/app/schemas/users.py | 3 +- backend/src/app/utils/dependencies.py | 4 +- backend/src/database/crud/projects.py | 16 +++--- .../src/database/crud/projects_students.py | 8 +-- .../test_database/test_crud/test_projects.py | 48 +++++++---------- .../test_crud/test_projects_students.py | 12 ++--- .../test_invites/test_invites.py | 1 - .../test_students/test_students.py | 1 - 18 files changed, 103 insertions(+), 149 deletions(-) diff --git a/backend/src/app/logic/invites.py b/backend/src/app/logic/invites.py index 85ed867d7..a835984ea 100644 --- a/backend/src/app/logic/invites.py +++ b/backend/src/app/logic/invites.py @@ -2,7 +2,7 @@ import settings import src.database.crud.invites as crud -from src.app.schemas.invites import InvitesLinkList, EmailAddress, NewInviteLink, InviteLink as InviteLinkModel +from src.app.schemas.invites import InvitesLinkList, EmailAddress, NewInviteLink from src.app.utils.mailto import generate_mailto_string from src.database.models import Edition, InviteLink as InviteLinkDB @@ -13,17 +13,8 @@ def delete_invite_link(db: Session, invite_link: InviteLinkDB): def get_pending_invites_page(db: Session, edition: Edition, page: int) -> InvitesLinkList: - """ - Query the database for a list of invite links - and wrap the result in a pydantic model - """ - invites_orm = crud.get_pending_invites_for_edition_page(db, edition, page) - invites = [] - for invite in invites_orm: - new_invite = InviteLinkModel(invite_link_id=invite.invite_link_id, - uuid=invite.uuid, target_email=invite.target_email, edition_name=edition.name) - invites.append(new_invite) - return InvitesLinkList(invite_links=invites) + """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)) def create_mailto_link(db: Session, edition: Edition, email_address: EmailAddress) -> NewInviteLink: diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index 27c57526e..3d2588167 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -1,57 +1,37 @@ from sqlalchemy.orm import Session +import src.database.crud.projects as crud from src.app.schemas.projects import ( - ProjectList, Project, ConflictStudentList, InputProject, Student, ConflictStudent, ConflictProject + ProjectList, ConflictStudentList, InputProject, ConflictStudent ) -from src.database.crud.projects import db_get_projects_for_edition_page, db_add_project, db_delete_project, \ - db_patch_project, db_get_conflict_students -from src.database.models import Edition +from src.database.models import Edition, Project -def logic_get_project_list(db: Session, edition: Edition, page: int) -> ProjectList: +def get_project_list(db: Session, edition: Edition, page: int) -> ProjectList: """Returns a list of all projects from a certain edition""" - db_all_projects = db_get_projects_for_edition_page(db, edition, page) - projects_model = [] - for project in db_all_projects: - project_model = Project(project_id=project.project_id, name=project.name, - number_of_students=project.number_of_students, - edition_name=project.edition.name, coaches=project.coaches, skills=project.skills, - partners=project.partners, project_roles=project.project_roles) - projects_model.append(project_model) - return ProjectList(projects=projects_model) - - -def logic_create_project(db: Session, edition: Edition, input_project: InputProject) -> Project: + return ProjectList(projects=crud.get_projects_for_edition_page(db, edition, page)) + + +def create_project(db: Session, edition: Edition, input_project: InputProject) -> Project: """Create a new project""" - project = db_add_project(db, edition, input_project) - return Project(project_id=project.project_id, name=project.name, number_of_students=project.number_of_students, - edition_name=project.edition.name, coaches=project.coaches, skills=project.skills, - partners=project.partners, project_roles=project.project_roles) + return crud.add_project(db, edition, input_project) -def logic_delete_project(db: Session, project_id: int): +def delete_project(db: Session, project_id: int): """Delete a project""" - db_delete_project(db, project_id) + crud.delete_project(db, project_id) -def logic_patch_project(db: Session, project_id: int, input_project: InputProject): +def patch_project(db: Session, project_id: int, input_project: InputProject): """Make changes to a project""" - db_patch_project(db, project_id, input_project) + crud.patch_project(db, project_id, input_project) -def logic_get_conflicts(db: Session, edition: Edition) -> ConflictStudentList: +def get_conflicts(db: Session, edition: Edition) -> ConflictStudentList: """Returns a list of all students together with the projects they are causing a conflict for""" - conflicts = db_get_conflict_students(db, edition) + conflicts = crud.get_conflict_students(db, edition) conflicts_model = [] for student, projects in conflicts: - projects_model = [] - for project in projects: - project_model = ConflictProject(project_id=project.project_id, name=project.name) - projects_model.append(project_model) - - conflicts_model.append(ConflictStudent(student=Student(student_id=student.student_id, - first_name=student.first_name, - last_name=student.last_name), - projects=projects_model)) + conflicts_model.append(ConflictStudent(student=student, projects=projects)) - return ConflictStudentList(conflict_students=conflicts_model, edition_name=edition.name) + return ConflictStudentList(conflict_students=conflicts_model) diff --git a/backend/src/app/logic/projects_students.py b/backend/src/app/logic/projects_students.py index 5ad72460d..12abaf612 100644 --- a/backend/src/app/logic/projects_students.py +++ b/backend/src/app/logic/projects_students.py @@ -1,18 +1,18 @@ from sqlalchemy.orm import Session + +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.logic.projects import logic_get_conflicts from src.app.schemas.projects import ConflictStudentList -from src.database.crud.projects_students import db_remove_student_project, db_add_student_project, \ - db_change_project_role, db_confirm_project_role from src.database.models import Project, ProjectRole, Student, Skill -def logic_remove_student_project(db: Session, project: Project, student_id: int): +def remove_student_project(db: Session, project: Project, student_id: int): """Remove a student from a project""" - db_remove_student_project(db, project, student_id) + crud.remove_student_project(db, project, student_id) -def logic_add_student_project(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): +def add_student_project(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): """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) \ @@ -31,10 +31,10 @@ def logic_add_student_project(db: Session, project: Project, student_id: int, sk if skill not in project.skills: raise FailedToAddProjectRoleException - db_add_student_project(db, project, student_id, skill_id, drafter_id) + crud.add_student_project(db, project, student_id, skill_id, drafter_id) -def logic_change_project_role(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): +def change_project_role(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): """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) \ @@ -54,15 +54,15 @@ def logic_change_project_role(db: Session, project: Project, student_id: int, sk if skill not in project.skills: raise FailedToAddProjectRoleException - db_change_project_role(db, project, student_id, skill_id, drafter_id) + crud.change_project_role(db, project, student_id, skill_id, drafter_id) -def logic_confirm_project_role(db: Session, project: Project, student_id: int): +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_get_conflicts(db, project.edition) + 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 - db_confirm_project_role(db, project, student_id) + crud.confirm_project_role(db, project, student_id) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index d299d8a83..f8a137d1b 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import Session import src.database.crud.users as users_crud -from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, UserRequest, user_model_to_schema +from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, user_model_to_schema from src.database.models import User @@ -18,10 +18,7 @@ def get_users_list(db: Session, admin: bool, edition_name: str | None, page: int else: users_orm = users_crud.get_users_for_edition_page(db, edition_name, page) - users = [] - for user in users_orm: - users.append(user_model_to_schema(user)) - return UsersListResponse(users=users) + return UsersListResponse(users=[user_model_to_schema(user) for user in users_orm]) def get_user_editions(db: Session, user: User) -> list[str]: @@ -66,12 +63,7 @@ def get_request_list(db: Session, edition_name: str | None, page: int) -> UserRe requests = users_crud.get_requests_page(db, page) else: requests = users_crud.get_requests_for_edition_page(db, edition_name, page) - - requests_model = [] - for request in requests: - user_req = UserRequest(request_id=request.request_id, edition_name=request.edition.name, user=request.user) - requests_model.append(user_req) - return UserRequestsResponse(requests=requests_model) + return UserRequestsResponse(requests=requests) def accept_request(db: Session, request_id: int): diff --git a/backend/src/app/routers/editions/invites/invites.py b/backend/src/app/routers/editions/invites/invites.py index 813bf7d9e..8f5446302 100644 --- a/backend/src/app/routers/editions/invites/invites.py +++ b/backend/src/app/routers/editions/invites/invites.py @@ -46,7 +46,4 @@ async def get_invite(invite_link: InviteLinkDB = Depends(get_invite_link)): Get a specific invitation link to see if it exists or not. Can be used to verify the validity of a link before granting a user access to the registration page. """ - model_invite_link = InviteLinkModel(invite_link_id=invite_link.invite_link_id, - uuid=invite_link.uuid, target_email=invite_link.target_email, - edition_name=invite_link.edition.name) - return model_invite_link + return invite_link diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index cb190cb34..7439e1f6d 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -3,8 +3,7 @@ from starlette import status from starlette.responses import Response -from src.app.logic.projects import logic_get_project_list, logic_create_project, logic_delete_project, \ - logic_patch_project, logic_get_conflicts +import src.app.logic.projects as logic from src.app.routers.tags import Tags from src.app.schemas.projects import ProjectList, Project, InputProject, ConflictStudentList from src.app.utils.dependencies import get_edition, get_project, require_admin, require_coach @@ -21,7 +20,7 @@ async def get_projects(db: Session = Depends(get_session), edition: Edition = De """ Get a list of all projects. """ - return logic_get_project_list(db, edition, page) + return logic.get_project_list(db, edition, page) @projects_router.post("/", status_code=status.HTTP_201_CREATED, response_model=Project, @@ -31,7 +30,7 @@ async def create_project(input_project: InputProject, """ Create a new project """ - return logic_create_project(db, edition, + return logic.create_project(db, edition, input_project) @@ -41,7 +40,7 @@ async def get_conflicts(db: Session = Depends(get_session), edition: Edition = D Get a list of all projects with conflicts, and the users that are causing those conflicts. """ - return logic_get_conflicts(db, edition) + return logic.get_conflicts(db, edition) @projects_router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, @@ -50,7 +49,7 @@ async def delete_project(project_id: int, db: Session = Depends(get_session)): """ Delete a specific project. """ - return logic_delete_project(db, project_id) + return logic.delete_project(db, project_id) @projects_router.get("/{project_id}", status_code=status.HTTP_200_OK, response_model=Project, @@ -59,11 +58,7 @@ async def get_project_route(project: ProjectModel = Depends(get_project)): """ Get information about a specific project. """ - project_model = Project(project_id=project.project_id, name=project.name, - number_of_students=project.number_of_students, - edition_name=project.edition.name, coaches=project.coaches, skills=project.skills, - partners=project.partners, project_roles=project.project_roles) - return project_model + return project @projects_router.patch("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, @@ -72,4 +67,4 @@ async def patch_project(project_id: int, input_project: InputProject, db: Sessio """ Update a project, changing some fields. """ - logic_patch_project(db, project_id, input_project) + logic.patch_project(db, project_id, input_project) 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 17e0813e9..8577f1d0a 100644 --- a/backend/src/app/routers/editions/projects/students/projects_students.py +++ b/backend/src/app/routers/editions/projects/students/projects_students.py @@ -3,8 +3,7 @@ from starlette import status from starlette.responses import Response -from src.app.logic.projects_students import logic_remove_student_project, logic_add_student_project, \ - logic_change_project_role, logic_confirm_project_role +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 @@ -21,7 +20,7 @@ async def remove_student_from_project(student_id: int, db: Session = Depends(get """ Remove a student from a project. """ - logic_remove_student_project(db, project, student_id) + logic.remove_student_project(db, project, student_id) @project_students_router.patch("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, @@ -31,7 +30,7 @@ async def change_project_role(student_id: int, input_sr: InputStudentRole, db: S """ 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) + logic.change_project_role(db, project, student_id, input_sr.skill_id, user.user_id) @project_students_router.post("/{student_id}", status_code=status.HTTP_201_CREATED, response_class=Response, @@ -43,7 +42,7 @@ async def add_student_to_project(student_id: int, input_sr: InputStudentRole, db 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) + 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, @@ -54,4 +53,4 @@ async def confirm_project_role(student_id: int, db: Session = Depends(get_sessio 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) + logic.confirm_project_role(db, project, student_id) diff --git a/backend/src/app/routers/editions/students/suggestions/students_suggestions.py b/backend/src/app/routers/editions/students/suggestions/students_suggestions.py index 8814b93e3..b842fe3fb 100644 --- a/backend/src/app/routers/editions/students/suggestions/students_suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/students_suggestions.py @@ -13,7 +13,7 @@ async def create_suggestion(edition_id: int, student_id: int): """ -@students_suggestions_router.get("/{suggestion_id}") +@students_suggestions_router.delete("/{suggestion_id}") async def delete_suggestion(edition_id: int, student_id: int, suggestion_id: int): """ Delete a suggestion you made about a student. diff --git a/backend/src/app/schemas/invites.py b/backend/src/app/schemas/invites.py index d3123f4fd..e0ff58f58 100644 --- a/backend/src/app/schemas/invites.py +++ b/backend/src/app/schemas/invites.py @@ -27,7 +27,6 @@ class InviteLink(CamelCaseModel): invite_link_id: int = Field(alias="id") uuid: UUID target_email: str = Field(alias="email") - edition_name: str class Config: """Set to ORM mode""" diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index 12c230111..3fe1d0991 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -53,7 +53,6 @@ class Project(CamelCaseModel): project_id: int name: str number_of_students: int - edition_name: str coaches: list[User] skills: list[Skill] @@ -71,12 +70,20 @@ class Student(CamelCaseModel): first_name: str last_name: str + class Config: + """Config Class""" + orm_mode = True + class ConflictProject(CamelCaseModel): """A project to be used in ConflictStudent""" project_id: int name: str + class Config: + """Config Class""" + orm_mode = True + class ProjectList(CamelCaseModel): """A list of projects""" @@ -88,11 +95,14 @@ class ConflictStudent(CamelCaseModel): student: Student projects: list[ConflictProject] + class Config: + """Config Class""" + orm_mode = True + class ConflictStudentList(CamelCaseModel): """A list of ConflictStudents""" conflict_students: list[ConflictStudent] - edition_name: str class InputProject(BaseModel): diff --git a/backend/src/app/schemas/users.py b/backend/src/app/schemas/users.py index 9acdf3104..de8fde5c5 100644 --- a/backend/src/app/schemas/users.py +++ b/backend/src/app/schemas/users.py @@ -1,3 +1,4 @@ +from src.app.schemas.editions import Edition from src.app.schemas.utils import CamelCaseModel from src.database.models import User as ModelUser @@ -54,7 +55,7 @@ class UserRequest(CamelCaseModel): """Model for a userrequest""" request_id: int - edition_name: str + edition: Edition user: User class Config: diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index 62678c42b..88f3fa110 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -10,7 +10,7 @@ from src.app.logic.security import ALGORITHM, get_user_by_id 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.projects import db_get_project +import src.database.crud.projects as crud_projects from src.database.database import get_session from src.database.models import Edition, InviteLink, User, Project @@ -95,4 +95,4 @@ def get_invite_link(invite_uuid: str, db: Session = Depends(get_session)) -> Inv def get_project(project_id: int, db: Session = Depends(get_session)) -> Project: """Get a project from het database, given the id in the path""" - return db_get_project(db, project_id) + return crud_projects.get_project(db, project_id) diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index 88308b347..682f22fd4 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -10,17 +10,17 @@ 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 db_get_projects_for_edition(db: Session, edition: Edition) -> list[Project]: +def get_projects_for_edition(db: Session, 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() -def db_get_projects_for_edition_page(db: Session, edition: Edition, page: int) -> list[Project]: +def get_projects_for_edition_page(db: Session, edition: Edition, page: int) -> list[Project]: """Returns a paginated list of all projects from a certain edition from the database""" return paginate(_get_projects_for_edition_query(db, edition), page).all() -def db_add_project(db: Session, edition: Edition, input_project: InputProject) -> Project: +def add_project(db: Session, edition: Edition, input_project: InputProject) -> Project: """ Add a project to the database If there are partner names that are not already in the database, add them @@ -43,12 +43,12 @@ def db_add_project(db: Session, edition: Edition, input_project: InputProject) - return project -def db_get_project(db: Session, project_id: int) -> Project: +def get_project(db: Session, 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() -def db_delete_project(db: Session, project_id: int): +def delete_project(db: Session, project_id: int): """Delete a specific project from the database""" # TODO: Maybe make the relationship between project and project_role cascade on delete? # so this code is handled by the database @@ -56,12 +56,12 @@ def db_delete_project(db: Session, project_id: int): for proj_role in proj_roles: db.delete(proj_role) - project = db_get_project(db, project_id) + project = get_project(db, project_id) db.delete(project) db.commit() -def db_patch_project(db: Session, project_id: int, input_project: InputProject): +def patch_project(db: Session, project_id: int, input_project: InputProject): """ Change some fields of a Project in the database If there are partner names that are not already in the database, add them @@ -87,7 +87,7 @@ def db_patch_project(db: Session, project_id: int, input_project: InputProject): db.commit() -def db_get_conflict_students(db: Session, edition: Edition) -> list[tuple[Student, list[Project]]]: +def get_conflict_students(db: Session, edition: Edition) -> list[tuple[Student, list[Project]]]: """ Query all students that are causing conflicts for a certain edition Return a ConflictStudent for each student that causes a conflict diff --git a/backend/src/database/crud/projects_students.py b/backend/src/database/crud/projects_students.py index 90d615bd2..5a3ea28b3 100644 --- a/backend/src/database/crud/projects_students.py +++ b/backend/src/database/crud/projects_students.py @@ -3,7 +3,7 @@ from src.database.models import Project, ProjectRole, Skill, User, Student -def db_remove_student_project(db: Session, project: Project, student_id: int): +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() @@ -11,7 +11,7 @@ def db_remove_student_project(db: Session, project: Project, student_id: int): db.commit() -def db_add_student_project(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): +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 @@ -25,7 +25,7 @@ def db_add_student_project(db: Session, project: Project, student_id: int, skill db.commit() -def db_change_project_role(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): +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 @@ -40,7 +40,7 @@ def db_change_project_role(db: Session, project: Project, student_id: int, skill db.commit() -def db_confirm_project_role(db: Session, project: Project, student_id: int): +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() diff --git a/backend/tests/test_database/test_crud/test_projects.py b/backend/tests/test_database/test_crud/test_projects.py index 09f41bbfa..d7e2b2b99 100644 --- a/backend/tests/test_database/test_crud/test_projects.py +++ b/backend/tests/test_database/test_crud/test_projects.py @@ -4,15 +4,7 @@ from settings import DB_PAGE_SIZE from src.app.schemas.projects import InputProject -from src.database.crud.projects import ( - db_get_projects_for_edition, - db_get_projects_for_edition_page, - db_add_project, - db_get_project, - db_delete_project, - db_patch_project, - db_get_conflict_students -) +import src.database.crud.projects as crud from src.database.models import Edition, Partner, Project, User, Skill, ProjectRole, Student @@ -66,14 +58,14 @@ def test_get_all_projects_empty(database_session: Session): edition: Edition = Edition(year=2022, name="ed2022") database_session.add(edition) database_session.commit() - projects: list[Project] = db_get_projects_for_edition( + projects: list[Project] = crud.get_projects_for_edition( database_session, edition) assert len(projects) == 0 def test_get_all_projects(database_with_data: Session, current_edition: Edition): """test get all projects""" - projects: list[Project] = db_get_projects_for_edition(database_with_data, current_edition) + projects: list[Project] = crud.get_projects_for_edition(database_with_data, current_edition) assert len(projects) == 3 @@ -86,8 +78,8 @@ def test_get_all_projects_pagination(database_session: Session): database_session.add(Project(name=f"Project {i}", edition=edition, number_of_students=5)) database_session.commit() - assert len(db_get_projects_for_edition_page(database_session, edition, 0)) == DB_PAGE_SIZE - assert len(db_get_projects_for_edition_page(database_session, edition, 1)) == round( + assert len(crud.get_projects_for_edition_page(database_session, edition, 0)) == DB_PAGE_SIZE + assert len(crud.get_projects_for_edition_page(database_session, edition, 1)) == round( DB_PAGE_SIZE * 1.5 ) - DB_PAGE_SIZE @@ -98,7 +90,7 @@ def test_add_project_partner_do_not_exist_yet(database_with_data: Session, curre partners=["ugent"], coaches=[1]) assert len(database_with_data.query(Partner).where( Partner.name == "ugent").all()) == 0 - new_project: Project = db_add_project( + 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() @@ -123,7 +115,7 @@ def test_add_project_partner_do_exist(database_with_data: Session, current_editi database_with_data.add(Partner(name="ugent")) assert len(database_with_data.query(Partner).where( Partner.name == "ugent").all()) == 1 - new_project: Project = db_add_project( + 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() @@ -144,12 +136,12 @@ def test_add_project_partner_do_exist(database_with_data: Session, current_editi def test_get_ghost_project(database_with_data: Session): """test project that don't exist""" with pytest.raises(NoResultFound): - db_get_project(database_with_data, 500) + crud.get_project(database_with_data, 500) def test_get_project(database_with_data: Session): """test get project""" - project: Project = db_get_project(database_with_data, 1) + project: Project = crud.get_project(database_with_data, 1) assert project.name == "project1" assert project.number_of_students == 2 @@ -158,10 +150,10 @@ def test_delete_project_no_project_roles(database_with_data: Session, current_ed """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(db_get_projects_for_edition(database_with_data, current_edition)) == 3 - db_delete_project(database_with_data, 3) - assert len(db_get_projects_for_edition(database_with_data, current_edition)) == 2 - assert 3 not in [project.project_id for project in db_get_projects_for_edition( + 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)] @@ -169,10 +161,10 @@ def test_delete_project_with_project_roles(database_with_data: Session, current_ """test delete a project that has project roles""" assert len(database_with_data.query(ProjectRole).where( ProjectRole.project_id == 1).all()) > 0 - assert len(db_get_projects_for_edition(database_with_data, current_edition)) == 3 - db_delete_project(database_with_data, 1) - assert len(db_get_projects_for_edition(database_with_data, current_edition)) == 2 - assert 1 not in [project.project_id for project in db_get_projects_for_edition( + 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 @@ -187,13 +179,13 @@ def test_patch_project(database_with_data: Session, current_edition: Edition): assert len(database_with_data.query(Partner).where( Partner.name == "ugent").all()) == 0 - new_project: Project = db_add_project( + 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() - db_patch_project(database_with_data, new_project.project_id, + crud.patch_project(database_with_data, new_project.project_id, proj_patched) assert new_partner in new_project.partners @@ -202,7 +194,7 @@ def test_patch_project(database_with_data: Session, current_edition: Edition): def test_get_conflict_students(database_with_data: Session, current_edition: Edition): """test if the right ConflictStudent is given""" - conflicts: list[(Student, list[Project])] = db_get_conflict_students(database_with_data, current_edition) + conflicts: list[(Student, list[Project])] = crud.get_conflict_students(database_with_data, current_edition) assert len(conflicts) == 1 assert conflicts[0][0].student_id == 1 assert len(conflicts[0][1]) == 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 a71ca4a52..854637a6c 100644 --- a/backend/tests/test_database/test_crud/test_projects_students.py +++ b/backend/tests/test_database/test_crud/test_projects_students.py @@ -3,7 +3,7 @@ from sqlalchemy.orm import Session from src.database.crud.projects_students import ( - db_remove_student_project, db_add_student_project, db_change_project_role) + remove_student_project, add_student_project, change_project_role) from src.database.models import Edition, Project, User, Skill, ProjectRole, Student @@ -52,7 +52,7 @@ def test_remove_student_from_project(database_with_data: Session): ProjectRole.student_id == 1).all()) == 2 project: Project = database_with_data.query( Project).where(Project.project_id == 1).one() - db_remove_student_project(database_with_data, project, 1) + remove_student_project(database_with_data, project, 1) assert len(database_with_data.query(ProjectRole).where( ProjectRole.student_id == 1).all()) == 1 @@ -62,7 +62,7 @@ def test_remove_student_from_project_not_assigned_to(database_with_data: Session project: Project = database_with_data.query( Project).where(Project.project_id == 2).one() with pytest.raises(NoResultFound): - db_remove_student_project(database_with_data, project, 2) + remove_student_project(database_with_data, project, 2) def test_add_student_project(database_with_data: Session): @@ -71,7 +71,7 @@ def test_add_student_project(database_with_data: Session): ProjectRole.student_id == 2).all()) == 1 project: Project = database_with_data.query( Project).where(Project.project_id == 2).one() - db_add_student_project(database_with_data, project, 2, 2, 1) + add_student_project(database_with_data, project, 2, 2, 1) assert len(database_with_data.query(ProjectRole).where( ProjectRole.student_id == 2).all()) == 2 @@ -85,7 +85,7 @@ def test_change_project_role(database_with_data: Session): 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 - db_change_project_role(database_with_data, project, 2, 2, 1) + change_project_role(database_with_data, project, 2, 2, 1) assert project_role.skill_id == 2 @@ -94,4 +94,4 @@ def test_change_project_role_not_assigned_to(database_with_data: Session): project: Project = database_with_data.query( Project).where(Project.project_id == 2).one() with pytest.raises(NoResultFound): - db_change_project_role(database_with_data, project, 2, 2, 1) + change_project_role(database_with_data, project, 2, 2, 1) 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 003816b94..30d3f48b6 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 @@ -39,7 +39,6 @@ def test_get_invites(database_session: Session, auth_client: AuthClient): link = json["inviteLinks"][0] assert link["id"] == 1 assert link["email"] == "test@ema.il" - assert link["editionName"] == "ed2022" def test_get_invites_paginated(database_session: Session, auth_client: AuthClient): 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 38bb32f32..d0785dbbf 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 @@ -301,7 +301,6 @@ def test_get_conflicts(database_with_data: Session, current_edition: Edition, au assert len(json['conflictStudents']) == 1 assert json['conflictStudents'][0]['student']['studentId'] == 1 assert len(json['conflictStudents'][0]['projects']) == 2 - assert json['editionName'] == "ed2022" def test_add_student_same_project_role(database_with_data: Session, current_edition: Edition, auth_client: AuthClient): From 12f42e874b2ef84018042fd868e513ea84463bf3 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sat, 9 Apr 2022 17:55:04 +0200 Subject: [PATCH 333/536] Test verification page redirects --- frontend/package.json | 2 +- frontend/src/Router.test.tsx | 29 +++++++++++++++ frontend/src/contexts/auth-context.tsx | 4 +-- frontend/src/tests/utils/contexts.tsx | 35 +++++++++++++++++++ frontend/src/views/LoginPage/LoginPage.tsx | 2 +- .../VerifyingTokenPage/VerifyingTokenPage.tsx | 6 +++- 6 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 frontend/src/Router.test.tsx create mode 100644 frontend/src/tests/utils/contexts.tsx diff --git a/frontend/package.json b/frontend/package.json index e13e51780..2e9ba6cd8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -55,7 +55,7 @@ "build": "react-scripts build", "docs": "typedoc", "lint": "eslint . --ext js,ts,jsx,tsx", - "test": "react-scripts test --watchAll=false" + "test": "react-scripts test --watchAll=false --verbose" }, "eslintConfig": { "extends": [ diff --git a/frontend/src/Router.test.tsx b/frontend/src/Router.test.tsx new file mode 100644 index 000000000..538cf21fb --- /dev/null +++ b/frontend/src/Router.test.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import Router from "./Router"; +import { defaultAuthState, TestAuthProvider } from "./tests/utils/contexts"; +import { render, screen } from "@testing-library/react"; + +test("isLoggedIn === null shows VerificationPage", () => { + const state = defaultAuthState(); + + render( + + + + ); + + expect(screen.getByTestId("verifying-page")).not.toBeNull(); +}); + +test("isLoggedIn === false shows LoginPage", () => { + const state = defaultAuthState(); + state.isLoggedIn = false; + + render( + + + + ); + + expect(screen.getByTestId("login-page")).not.toBeNull(); +}); diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx index 03b94021b..c5543c73d 100644 --- a/frontend/src/contexts/auth-context.tsx +++ b/frontend/src/contexts/auth-context.tsx @@ -31,7 +31,7 @@ function authDefaultState(): AuthContextState { role: null, setRole: (_: Role | null) => {}, userId: null, - setUserId: (value: number | null) => {}, + setUserId: (_: number | null) => {}, token: getToken(), setToken: (_: string | null) => {}, editions: [], @@ -39,7 +39,7 @@ function authDefaultState(): AuthContextState { }; } -const AuthContext = React.createContext(authDefaultState()); +export const AuthContext = React.createContext(authDefaultState()); /** * Custom React hook to use our authentication context. diff --git a/frontend/src/tests/utils/contexts.tsx b/frontend/src/tests/utils/contexts.tsx new file mode 100644 index 000000000..06e65a187 --- /dev/null +++ b/frontend/src/tests/utils/contexts.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { AuthContext, AuthContextState } from "../../contexts/auth-context"; +import "@testing-library/jest-dom"; + +/** + * Initial state to return & modify before passing into the provider + */ +export function defaultAuthState(): AuthContextState { + return { + isLoggedIn: null, + setIsLoggedIn: jest.fn(), + role: null, + setRole: jest.fn(), + userId: null, + setUserId: jest.fn(), + token: null, + setToken: jest.fn(), + editions: [], + setEditions: jest.fn(), + }; +} + +/** + * AuthProvider to be used in unit testing + */ + +export function TestAuthProvider({ + children, + state, +}: { + children: React.ReactNode; + state: AuthContextState; +}) { + return {children}; +} diff --git a/frontend/src/views/LoginPage/LoginPage.tsx b/frontend/src/views/LoginPage/LoginPage.tsx index 610e5aebc..76d60064f 100644 --- a/frontend/src/views/LoginPage/LoginPage.tsx +++ b/frontend/src/views/LoginPage/LoginPage.tsx @@ -43,7 +43,7 @@ export default function LoginPage() { } return ( -
                                        +
                                        diff --git a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx index 6c95c7afc..ddb3fd731 100644 --- a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx +++ b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx @@ -36,5 +36,9 @@ export default function VerifyingTokenPage() { }, [authContext]); // This will be replaced later on - return

                                        Loading...

                                        ; + return ( +
                                        +

                                        Loading...

                                        +
                                        + ); } From b4cfa276b3df03b521b11ffa48ce3508e324f514 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sat, 9 Apr 2022 18:00:08 +0200 Subject: [PATCH 334/536] Add more utility stuff --- frontend/src/Router.test.tsx | 21 +++++---------------- frontend/src/tests/utils/renderer.tsx | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 16 deletions(-) create mode 100644 frontend/src/tests/utils/renderer.tsx diff --git a/frontend/src/Router.test.tsx b/frontend/src/Router.test.tsx index 538cf21fb..bdf89fe3e 100644 --- a/frontend/src/Router.test.tsx +++ b/frontend/src/Router.test.tsx @@ -1,17 +1,11 @@ -import React from "react"; -import Router from "./Router"; -import { defaultAuthState, TestAuthProvider } from "./tests/utils/contexts"; -import { render, screen } from "@testing-library/react"; +import { defaultAuthState } from "./tests/utils/contexts"; +import { screen } from "@testing-library/react"; +import { contextRender } from "./tests/utils/renderer"; test("isLoggedIn === null shows VerificationPage", () => { const state = defaultAuthState(); - render( - - - - ); - + contextRender(state); expect(screen.getByTestId("verifying-page")).not.toBeNull(); }); @@ -19,11 +13,6 @@ test("isLoggedIn === false shows LoginPage", () => { const state = defaultAuthState(); state.isLoggedIn = false; - render( - - - - ); - + contextRender(state); expect(screen.getByTestId("login-page")).not.toBeNull(); }); diff --git a/frontend/src/tests/utils/renderer.tsx b/frontend/src/tests/utils/renderer.tsx new file mode 100644 index 000000000..9daeec532 --- /dev/null +++ b/frontend/src/tests/utils/renderer.tsx @@ -0,0 +1,17 @@ +import { AuthContextState } from "../../contexts"; +import { TestAuthProvider } from "./contexts"; +import Router from "../../Router"; +import React from "react"; +import { render } from "@testing-library/react"; + +/** + * Custom renderer that adds a custom AuthProvider that can be + * manipulated on the fly to force different scenarios + */ +export function contextRender(state: AuthContextState) { + render( + + + + ); +} From 081719575494b325fc2e63ce41078d011f489908 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Sat, 9 Apr 2022 18:04:53 +0200 Subject: [PATCH 335/536] fix TODOs --- backend/src/app/logic/security.py | 14 +------------- backend/src/app/utils/dependencies.py | 5 +++-- backend/src/database/crud/editions.py | 1 - backend/src/database/crud/projects.py | 2 -- backend/src/database/crud/users.py | 13 ++++++++++++- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/backend/src/app/logic/security.py b/backend/src/app/logic/security.py index af6789c03..2320ebc08 100644 --- a/backend/src/app/logic/security.py +++ b/backend/src/app/logic/security.py @@ -7,6 +7,7 @@ import settings from src.app.exceptions.authentication import InvalidCredentialsException from src.database import models +from src.database.crud.users import get_user_by_email # Configuration ALGORITHM = "HS256" @@ -39,19 +40,6 @@ def get_password_hash(password: str) -> str: return pwd_context.hash(password) -# TODO remove this when the users crud has been implemented -def get_user_by_email(db: Session, email: str) -> models.User: - """Find a user by their email address""" - auth_email = db.query(models.AuthEmail).where(models.AuthEmail.email == email).one() - return db.query(models.User).where(models.User.user_id == auth_email.user_id).one() - - -# TODO remove this when the users crud has been implemented -def get_user_by_id(db: Session, user_id: int) -> models.User: - """Find a user by their id""" - return db.query(models.User).where(models.User.user_id == user_id).one() - - def authenticate_user(db: Session, email: str, password: str) -> models.User: """Match an email/password combination to a User model""" user = get_user_by_email(db, email) diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index 88f3fa110..55b2eab3f 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -5,12 +5,13 @@ from sqlalchemy.orm import Session import settings +import src.database.crud.projects as crud_projects from src.app.exceptions.authentication import ExpiredCredentialsException, InvalidCredentialsException, \ MissingPermissionsException -from src.app.logic.security import ALGORITHM, get_user_by_id +from src.app.logic.security import ALGORITHM from src.database.crud.editions import get_edition_by_name from src.database.crud.invites import get_invite_link_by_uuid -import src.database.crud.projects as crud_projects +from src.database.crud.users import get_user_by_id from src.database.database import get_session from src.database.models import Edition, InviteLink, User, Project diff --git a/backend/src/database/crud/editions.py b/backend/src/database/crud/editions.py index 861377e24..5bd0bd56d 100644 --- a/backend/src/database/crud/editions.py +++ b/backend/src/database/crud/editions.py @@ -18,7 +18,6 @@ def get_edition_by_name(db: Session, edition_name: str) -> Edition: Returns: Edition: an edition if found else an exception is raised """ - # TODO: check that name is valid return db.query(Edition).where(Edition.name == edition_name).one() diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index 682f22fd4..390579f62 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -50,8 +50,6 @@ def get_project(db: Session, project_id: int) -> Project: def delete_project(db: Session, project_id: int): """Delete a specific project from the database""" - # TODO: Maybe make the relationship between project and project_role cascade on delete? - # so this code is handled by the database proj_roles = db.query(ProjectRole).where(ProjectRole.project_id == project_id).all() for proj_role in proj_roles: db.delete(proj_role) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 64c6912f6..42984ce21 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -1,9 +1,9 @@ from sqlalchemy.orm import Session, Query from src.database.crud.editions import get_edition_by_name +from src.database.crud.editions import get_editions from src.database.crud.util import paginate from src.database.models import user_editions, User, Edition, CoachRequest, AuthGoogle, AuthEmail, AuthGitHub -from src.database.crud.editions import get_editions def _get_admins_query(db: Session) -> Query: @@ -167,3 +167,14 @@ def reject_request(db: Session, request_id: int): Remove request """ db.query(CoachRequest).where(CoachRequest.request_id == request_id).delete() + + +def get_user_by_email(db: Session, 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() + + +def get_user_by_id(db: Session, user_id: int) -> User: + """Find a user by their id""" + return db.query(User).where(User.user_id == user_id).one() From f0ca2df8f3510574799145a5c5a4b6006a15b881 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 10 Apr 2022 13:46:11 +0200 Subject: [PATCH 336/536] Add name filter for GET users --- backend/src/app/logic/users.py | 17 +++-- backend/src/app/routers/users/users.py | 10 ++- backend/src/database/crud/users.py | 27 ++++--- .../test_database/test_crud/test_users.py | 71 +++++++++++++++++-- .../test_routers/test_users/test_users.py | 57 +++++++++++++++ 5 files changed, 158 insertions(+), 24 deletions(-) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index d299d8a83..1613692cd 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -5,18 +5,27 @@ from src.database.models import User -def get_users_list(db: Session, admin: bool, edition_name: str | None, page: int) -> UsersListResponse: +def get_users_list( + db: Session, + admin: bool, + edition_name: str | None, + name: str | None, + page: int +) -> UsersListResponse: """ Query the database for a list of users and wrap the result in a pydantic model """ + if name is None: + name = "" + if admin: - users_orm = users_crud.get_admins_page(db, page) + users_orm = users_crud.get_admins_page(db, page, name) else: if edition_name is None: - users_orm = users_crud.get_users_page(db, page) + users_orm = users_crud.get_users_page(db, page, name) else: - users_orm = users_crud.get_users_for_edition_page(db, edition_name, page) + users_orm = users_crud.get_users_for_edition_page(db, edition_name, page, name) users = [] for user in users_orm: diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index 9062e9221..9ae446189 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -13,15 +13,19 @@ @users_router.get("/", response_model=UsersListResponse, dependencies=[Depends(require_admin)]) -async def get_users(admin: bool = Query(False), edition: str | None = Query(None), page: int = 0, - db: Session = Depends(get_session)): +async def get_users( + admin: bool = Query(False), + edition: str | None = Query(None), + name: str | None = Query(None), + page: int = 0, + db: Session = Depends(get_session)): """ Get users When the admin parameter is True, the edition parameter will have no effect. Since admins have access to all editions. """ - return logic.get_users_list(db, admin, edition, page) + return logic.get_users_list(db, admin, edition, name, page) @users_router.get("/current", response_model=UserData) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 64c6912f6..ad63dee92 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -6,9 +6,10 @@ from src.database.crud.editions import get_editions -def _get_admins_query(db: Session) -> Query: +def _get_admins_query(db: Session, name: str = "") -> Query: return db.query(User) \ .where(User.admin) \ + .where(User.name.contains(name)) \ .join(AuthEmail, isouter=True) \ .join(AuthGitHub, isouter=True) \ .join(AuthGoogle, isouter=True) @@ -21,15 +22,15 @@ def get_admins(db: Session) -> list[User]: return _get_admins_query(db).all() -def get_admins_page(db: Session, page: int) -> list[User]: +def get_admins_page(db: Session, page: int, name: str = "") -> list[User]: """ Get all admins paginated """ - return paginate(_get_admins_query(db), page).all() + return paginate(_get_admins_query(db, name), page).all() -def _get_users_query(db: Session) -> Query: - return db.query(User) +def _get_users_query(db: Session, name: str = "") -> Query: + return db.query(User).where(User.name.contains(name)) def get_users(db: Session) -> list[User]: @@ -37,9 +38,9 @@ def get_users(db: Session) -> list[User]: return _get_users_query(db).all() -def get_users_page(db: Session, page: int) -> list[User]: +def get_users_page(db: Session, page: int, name: str = "") -> list[User]: """Get all users (coaches + admins) paginated""" - return paginate(_get_users_query(db), page).all() + return paginate(_get_users_query(db, name), page).all() def get_user_edition_names(db: Session, user: User) -> list[str]: @@ -58,8 +59,12 @@ def get_user_edition_names(db: Session, user: User) -> list[str]: return editions -def _get_users_for_edition_query(db: Session, edition: Edition) -> Query: - return db.query(User).join(user_editions).filter(user_editions.c.edition_id == edition.edition_id) +def _get_users_for_edition_query(db: Session, edition: Edition, name = "") -> Query: + return db\ + .query(User)\ + .where(User.name.contains(name))\ + .join(user_editions)\ + .filter(user_editions.c.edition_id == edition.edition_id) def get_users_for_edition(db: Session, edition_name: str) -> list[User]: @@ -69,11 +74,11 @@ def get_users_for_edition(db: Session, edition_name: str) -> list[User]: return _get_users_for_edition_query(db, get_edition_by_name(db, edition_name)).all() -def get_users_for_edition_page(db: Session, edition_name: str, page: int) -> list[User]: +def get_users_for_edition_page(db: Session, edition_name: str, page: int, name = "") -> list[User]: """ Get all coaches from the given edition """ - return paginate(_get_users_for_edition_query(db, get_edition_by_name(db, edition_name)), page).all() + return paginate(_get_users_for_edition_query(db, get_edition_by_name(db, edition_name), name), page).all() def edit_admin_status(db: Session, user_id: int, admin: bool): diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index 4bb3da470..f10c515c8 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -59,7 +59,7 @@ def test_get_all_users(database_session: Session, data: dict[str, int]): def test_get_all_users_paginated(database_session: Session): for i in range(round(DB_PAGE_SIZE * 1.5)): - database_session.add(models.User(name=f"Project {i}", admin=False)) + database_session.add(models.User(name=f"User {i}", admin=False)) database_session.commit() assert len(users_crud.get_users_page(database_session, 0)) == DB_PAGE_SIZE @@ -68,6 +68,19 @@ def test_get_all_users_paginated(database_session: Session): ) - DB_PAGE_SIZE +def test_get_all_users_paginated_filter_name(database_session: Session): + 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() + + assert len(users_crud.get_users_page(database_session, 0, name="1")) == count + assert len(users_crud.get_users_page(database_session, 1, name="1")) == max(count - round( + DB_PAGE_SIZE * 1.5), 0) + + def test_get_all_admins(database_session: Session, data: dict[str, str]): """Test get request for admins""" @@ -78,14 +91,28 @@ def test_get_all_admins(database_session: Session, data: dict[str, str]): def test_get_all_admins_paginated(database_session: Session): + 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() + + assert len(users_crud.get_admins_page(database_session, 0)) == min(count, DB_PAGE_SIZE) + assert len(users_crud.get_admins_page(database_session, 1)) == min(count - DB_PAGE_SIZE, DB_PAGE_SIZE) + + +def test_get_all_admins_paginated_filter_name(database_session: Session): + count = 0 for i in range(round(DB_PAGE_SIZE * 1.5)): - database_session.add(models.User(name=f"Project {i}", admin=True)) + 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() - assert len(users_crud.get_admins_page(database_session, 0)) == DB_PAGE_SIZE - assert len(users_crud.get_admins_page(database_session, 1)) == round( - DB_PAGE_SIZE * 1.5 - ) - DB_PAGE_SIZE + assert len(users_crud.get_admins_page(database_session, 0, name="1")) == count + assert len(users_crud.get_admins_page(database_session, 1, name="1")) == max(count - round( + DB_PAGE_SIZE * 1.5), 0) def test_get_user_edition_names_empty(database_session: Session): @@ -180,6 +207,38 @@ def test_get_all_users_for_edition_paginated(database_session: Session): ) - DB_PAGE_SIZE +def test_get_all_users_for_edition_paginated_filter_name(database_session: Session): + 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() + + count = 0 + 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(), [ + {"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() + + assert len(users_crud.get_users_for_edition_page(database_session, edition_1.name, 0, name="1")) == \ + min(count, DB_PAGE_SIZE) + assert len(users_crud.get_users_for_edition_page(database_session, edition_1.name, 1, name="1")) == \ + max(count - round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE, 0) + assert len(users_crud.get_users_for_edition_page(database_session, edition_2.name, 0, name="1")) == \ + min(count, DB_PAGE_SIZE) + assert len(users_crud.get_users_for_edition_page(database_session, edition_2.name, 1, name="1")) == \ + max(count - DB_PAGE_SIZE, 0) + + def test_edit_admin_status(database_session: Session): """Test changing the admin status of a user""" diff --git a/backend/tests/test_routers/test_users/test_users.py b/backend/tests/test_routers/test_users/test_users.py index 9690b83ee..59c799e7f 100644 --- a/backend/tests/test_routers/test_users/test_users.py +++ b/backend/tests/test_routers/test_users/test_users.py @@ -82,6 +82,24 @@ def test_get_all_users_paginated(database_session: Session, auth_client: AuthCli # +1 because Authclient.admin() also creates one user. +def test_get_all_users_paginated_filter_name(database_session: Session, 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() + + 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) + + def test_get_users_response(database_session: Session, auth_client: AuthClient, data: dict[str, str]): """Test the response model of a user""" auth_client.admin() @@ -108,6 +126,25 @@ def test_get_all_admins(database_session: Session, auth_client: AuthClient, data def test_get_all_admins_paginated(database_session: Session, 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() + + 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. + + +def test_get_all_admins_paginated_filter_name(database_session: Session, 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() @@ -150,6 +187,26 @@ def test_get_all_users_for_edition_paginated(database_session: Session, auth_cli # +1 because Authclient.admin() also creates one user. +def test_get_all_users_for_edition_paginated_filter_user(database_session: Session, 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) + + count = 0 + for i in range(round(DB_PAGE_SIZE * 1.5)): + database_session.add(models.User(name=f"User {i}", admin=False, editions=[edition])) + if "1" in str(i): + count += 1 + 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) + def test_get_admins_from_edition(database_session: Session, auth_client: AuthClient, data: dict[str, str | int]): """Test endpoint for getting a list of admins, edition should be ignored""" auth_client.admin() From 2789025c1c2a86a8afd72ea27305950e728b4261 Mon Sep 17 00:00:00 2001 From: beguille Date: Sun, 10 Apr 2022 15:05:41 +0200 Subject: [PATCH 337/536] moved readonly check to router dependency --- backend/src/app/logic/invites.py | 3 --- backend/src/app/logic/projects.py | 11 ++-------- backend/src/app/logic/projects_students.py | 17 ++++----------- backend/src/app/logic/register.py | 3 --- .../app/routers/editions/invites/invites.py | 4 ++-- .../app/routers/editions/projects/projects.py | 17 +++++++-------- .../projects/students/projects_students.py | 21 ++++++++++--------- .../app/routers/editions/register/register.py | 4 ++-- .../app/routers/editions/webhooks/webhooks.py | 4 ++-- backend/src/app/utils/dependencies.py | 9 ++++++++ backend/src/app/utils/edition_readonly.py | 12 ----------- backend/src/database/crud/webhooks.py | 3 --- 12 files changed, 40 insertions(+), 68 deletions(-) delete mode 100644 backend/src/app/utils/edition_readonly.py diff --git a/backend/src/app/logic/invites.py b/backend/src/app/logic/invites.py index 04d619116..b5b98abdc 100644 --- a/backend/src/app/logic/invites.py +++ b/backend/src/app/logic/invites.py @@ -1,7 +1,6 @@ from sqlalchemy.orm import Session from src.app.schemas.invites import InvitesListResponse, EmailAddress, NewInviteLink, InviteLink as InviteLinkModel -from src.app.utils.edition_readonly import check_readonly_edition from src.app.utils.mailto import generate_mailto_string from src.database.crud.invites import create_invite_link, delete_invite_link as delete_link_db, get_all_pending_invites from src.database.models import Edition, InviteLink as InviteLinkDB @@ -30,8 +29,6 @@ def get_pending_invites_list(db: Session, edition: Edition) -> InvitesListRespon def create_mailto_link(db: Session, edition: Edition, email_address: EmailAddress) -> NewInviteLink: """Add a new invite link into the database & return a mailto link for it""" - check_readonly_edition(db, edition) - # Create db entry new_link_db = create_invite_link(db, edition, email_address.email) diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index 9a14e0c6e..6d51d102a 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -2,7 +2,6 @@ from src.app.schemas.projects import ProjectList, Project, ConflictStudentList, InputProject, Student, \ ConflictStudent, ConflictProject -from src.app.utils.edition_readonly import check_readonly_edition from src.database.crud.projects import db_get_all_projects, db_add_project, db_delete_project, \ db_patch_project, db_get_conflict_students from src.database.models import Edition, Project as ProjectModel @@ -23,25 +22,19 @@ def logic_get_project_list(db: Session, edition: Edition) -> ProjectList: def logic_create_project(db: Session, edition: Edition, input_project: InputProject) -> Project: """Create a new project""" - check_readonly_edition(db, edition) - project = db_add_project(db, edition, input_project) return Project(project_id=project.project_id, name=project.name, number_of_students=project.number_of_students, edition_name=project.edition.name, coaches=project.coaches, skills=project.skills, partners=project.partners, project_roles=project.project_roles) -def logic_delete_project(db: Session, project_id: int, edition: Edition): +def logic_delete_project(db: Session, project_id: int): """Delete a project""" - check_readonly_edition(db, edition) - db_delete_project(db, project_id) -def logic_patch_project(db: Session, project_id: int, input_project: InputProject, edition: Edition): +def logic_patch_project(db: Session, project_id: int, input_project: InputProject): """Make changes to a project""" - check_readonly_edition(db, edition) - db_patch_project(db, project_id, input_project) diff --git a/backend/src/app/logic/projects_students.py b/backend/src/app/logic/projects_students.py index 5d040aa39..01ef8d494 100644 --- a/backend/src/app/logic/projects_students.py +++ b/backend/src/app/logic/projects_students.py @@ -3,24 +3,18 @@ from src.app.logic.projects import logic_get_conflicts from src.app.schemas.projects import ConflictStudentList -from src.app.utils.edition_readonly import check_readonly_edition from src.database.crud.projects_students import db_remove_student_project, db_add_student_project, \ db_change_project_role, db_confirm_project_role -from src.database.models import Project, ProjectRole, Student, Skill, Edition +from src.database.models import Project, ProjectRole, Student, Skill -def logic_remove_student_project(db: Session, project: Project, student_id: int, edition: Edition): +def logic_remove_student_project(db: Session, project: Project, student_id: int): """Remove a student from a project""" - check_readonly_edition(db, edition) - db_remove_student_project(db, project, student_id) -def logic_add_student_project(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int, - edition: Edition): +def logic_add_student_project(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): """Add a student to a project""" - check_readonly_edition(db, edition) - # 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: @@ -41,11 +35,8 @@ def logic_add_student_project(db: Session, project: Project, student_id: int, sk db_add_student_project(db, project, student_id, skill_id, drafter_id) -def logic_change_project_role(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int, - edition: Edition): +def logic_change_project_role(db: Session, project: Project, student_id: int, skill_id: int, drafter_id: int): """Change the role of the student in the project""" - check_readonly_edition(db, edition) - # 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: diff --git a/backend/src/app/logic/register.py b/backend/src/app/logic/register.py index 71421e498..9a1a5c3bb 100644 --- a/backend/src/app/logic/register.py +++ b/backend/src/app/logic/register.py @@ -2,7 +2,6 @@ from sqlalchemy.orm import Session from src.app.schemas.register import NewUser -from src.app.utils.edition_readonly import check_readonly_edition from src.database.models import Edition, InviteLink from src.database.crud.register import create_coach_request, create_user, create_auth_email from src.database.crud.invites import get_invite_link_by_uuid, delete_invite_link @@ -12,8 +11,6 @@ def create_request(db: Session, new_user: NewUser, edition: Edition) -> None: """Create a coach request. If something fails, the changes aren't committed""" - check_readonly_edition(db, edition) - invite_link: InviteLink = get_invite_link_by_uuid(db, new_user.uuid) with db.begin_nested() as transaction: diff --git a/backend/src/app/routers/editions/invites/invites.py b/backend/src/app/routers/editions/invites/invites.py index e500491c9..46021cee5 100644 --- a/backend/src/app/routers/editions/invites/invites.py +++ b/backend/src/app/routers/editions/invites/invites.py @@ -6,7 +6,7 @@ from src.app.logic.invites import create_mailto_link, delete_invite_link, get_pending_invites_list from src.app.routers.tags import Tags from src.app.schemas.invites import InvitesListResponse, EmailAddress, NewInviteLink, InviteLink as InviteLinkModel -from src.app.utils.dependencies import get_edition, get_invite_link, require_admin +from src.app.utils.dependencies import get_edition, get_invite_link, require_admin, check_latest_edition from src.database.database import get_session from src.database.models import Edition, InviteLink as InviteLinkDB @@ -22,7 +22,7 @@ async def get_invites(db: Session = Depends(get_session), edition: Edition = Dep @invites_router.post("/", status_code=status.HTTP_201_CREATED, response_model=NewInviteLink, - dependencies=[Depends(require_admin)]) + dependencies=[Depends(require_admin), Depends(check_latest_edition)]) async def create_invite(email: EmailAddress, db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): """ diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index 2113ab581..0faf27711 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -8,7 +8,7 @@ from src.app.routers.tags import Tags from src.app.schemas.projects import ProjectList, Project, InputProject, \ ConflictStudentList -from src.app.utils.dependencies import get_edition, get_project, require_admin, require_coach +from src.app.utils.dependencies import get_edition, get_project, require_admin, require_coach, check_latest_edition from src.database.database import get_session from src.database.models import Edition, Project as ProjectModel from .students import project_students_router @@ -26,7 +26,7 @@ async def get_projects(db: Session = Depends(get_session), edition: Edition = De @projects_router.post("/", status_code=status.HTTP_201_CREATED, response_model=Project, - dependencies=[Depends(require_admin)]) + dependencies=[Depends(require_admin), Depends(check_latest_edition)]) async def create_project(input_project: InputProject, db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): """ @@ -46,12 +46,12 @@ async def get_conflicts(db: Session = Depends(get_session), edition: Edition = D @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), edition: Edition = Depends(get_edition)): + dependencies=[Depends(require_admin), Depends(check_latest_edition)]) +async def delete_project(project_id: int, db: Session = Depends(get_session)): """ Delete a specific project. """ - return logic_delete_project(db, project_id, edition) + return logic_delete_project(db, project_id) @projects_router.get("/{project_id}", status_code=status.HTTP_200_OK, response_model=Project, @@ -68,10 +68,9 @@ async def get_project_route(project: ProjectModel = Depends(get_project)): @projects_router.patch("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, - dependencies=[Depends(require_admin)]) -async def patch_project(project_id: int, input_project: InputProject, db: Session = Depends(get_session), - edition: Edition = Depends(get_edition)): + dependencies=[Depends(require_admin), Depends(check_latest_edition)]) +async def patch_project(project_id: int, input_project: InputProject, db: Session = Depends(get_session)): """ Update a project, changing some fields. """ - logic_patch_project(db, project_id, input_project, edition) + logic_patch_project(db, project_id, input_project) 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 5e907b3f1..8f22d51f3 100644 --- a/backend/src/app/routers/editions/projects/students/projects_students.py +++ b/backend/src/app/routers/editions/projects/students/projects_students.py @@ -7,7 +7,7 @@ logic_change_project_role, logic_confirm_project_role 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_edition +from src.app.utils.dependencies import get_project, require_admin, require_coach, get_edition, check_latest_edition from src.database.database import get_session from src.database.models import Project, User, Edition @@ -15,26 +15,27 @@ @project_students_router.delete("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, - dependencies=[Depends(require_coach)]) + dependencies=[Depends(require_coach), Depends(check_latest_edition)]) async def remove_student_from_project(student_id: int, db: Session = Depends(get_session), - project: Project = Depends(get_project), edition: Edition = Depends(get_edition)): + project: Project = Depends(get_project)): """ Remove a student from a project. """ - logic_remove_student_project(db, project, student_id, edition) + logic_remove_student_project(db, project, student_id) -@project_students_router.patch("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response) +@project_students_router.patch("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, + dependencies=[Depends(check_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), - edition: Edition = Depends(get_edition)): + project: Project = Depends(get_project), 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, edition) + logic_change_project_role(db, project, student_id, input_sr.skill_id, user.user_id) -@project_students_router.post("/{student_id}", status_code=status.HTTP_201_CREATED, response_class=Response) +@project_students_router.post("/{student_id}", status_code=status.HTTP_201_CREATED, response_class=Response, + dependencies=[Depends(check_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), edition: Edition = Depends(get_edition)): @@ -43,7 +44,7 @@ async def add_student_to_project(student_id: int, input_sr: InputStudentRole, db 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, edition) + 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, diff --git a/backend/src/app/routers/editions/register/register.py b/backend/src/app/routers/editions/register/register.py index 3e333ff8e..ccac62e83 100644 --- a/backend/src/app/routers/editions/register/register.py +++ b/backend/src/app/routers/editions/register/register.py @@ -5,14 +5,14 @@ from src.app.logic.register import create_request from src.app.routers.tags import Tags from src.app.schemas.register import NewUser -from src.app.utils.dependencies import get_edition +from src.app.utils.dependencies import get_edition, check_latest_edition from src.database.database import get_session from src.database.models import Edition registration_router = APIRouter(prefix="/register", tags=[Tags.REGISTRATION]) -@registration_router.post("/email", status_code=status.HTTP_201_CREATED) +@registration_router.post("/email", status_code=status.HTTP_201_CREATED, dependencies=[Depends(check_latest_edition)]) async def register_email(user: NewUser, db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): """ Register a new account using the email/password format. diff --git a/backend/src/app/routers/editions/webhooks/webhooks.py b/backend/src/app/routers/editions/webhooks/webhooks.py index 729c8aff9..5f0d4a9e8 100644 --- a/backend/src/app/routers/editions/webhooks/webhooks.py +++ b/backend/src/app/routers/editions/webhooks/webhooks.py @@ -5,7 +5,7 @@ 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 +from src.app.utils.dependencies import get_edition, require_admin, check_latest_edition from src.database.crud.webhooks import get_webhook, create_webhook from src.database.database import get_session from src.database.models import Edition @@ -19,7 +19,7 @@ def valid_uuid(uuid: str, database: Session = Depends(get_session)): @webhooks_router.post("/", response_model=WebhookUrlResponse, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(require_admin)]) + dependencies=[Depends(require_admin), Depends(check_latest_edition)]) def new(edition: Edition = Depends(get_edition), database: Session = Depends(get_session)): """Create a new webhook for an edition""" return create_webhook(database, edition) diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index 05ac8628e..1133f502b 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -7,6 +7,7 @@ import settings from src.app.exceptions.authentication import ExpiredCredentialsException, InvalidCredentialsException, \ MissingPermissionsException +from src.app.exceptions.editions import ReadOnlyEditionException from src.app.logic.security import ALGORITHM, get_user_by_id from src.database.crud.editions import get_edition_by_name, latest_edition from src.database.crud.projects import db_get_project @@ -20,6 +21,14 @@ def get_edition(edition_name: str, database: Session = Depends(get_session)) -> return get_edition_by_name(database, edition_name) +def check_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)""" + latest = latest_edition(database) + if edition != latest: + raise ReadOnlyEditionException + return latest + + oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login/token") diff --git a/backend/src/app/utils/edition_readonly.py b/backend/src/app/utils/edition_readonly.py deleted file mode 100644 index 849fbd43a..000000000 --- a/backend/src/app/utils/edition_readonly.py +++ /dev/null @@ -1,12 +0,0 @@ -from sqlalchemy.orm import Session - -from src.app.exceptions.editions import ReadOnlyEditionException -from src.database.crud.editions import latest_edition -from src.database.models import Edition - - -def check_readonly_edition(db: Session, edition: Edition): - """Checks if the given edition is the latest one (others are read-only)""" - latest = latest_edition(db) - if edition != latest: - raise ReadOnlyEditionException diff --git a/backend/src/database/crud/webhooks.py b/backend/src/database/crud/webhooks.py index c0e7a9d30..f2e25e98b 100644 --- a/backend/src/database/crud/webhooks.py +++ b/backend/src/database/crud/webhooks.py @@ -1,6 +1,5 @@ from sqlalchemy.orm import Session -from src.app.utils.edition_readonly import check_readonly_edition from src.database.models import WebhookURL, Edition @@ -11,8 +10,6 @@ def get_webhook(database: Session, uuid: str) -> WebhookURL: def create_webhook(database: Session, edition: Edition) -> WebhookURL: """Create a webhook for a given edition""" - check_readonly_edition(database, edition) - webhook_url: WebhookURL = WebhookURL(edition=edition) database.add(webhook_url) database.commit() From 923bf77a5404145df49ea5e7c211284705b60398 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Sun, 10 Apr 2022 15:43:18 +0200 Subject: [PATCH 338/536] added get of emails --- ...fa0af0a_added_to_the_right_side_of_the_.py | 28 ++++++++++++ .../54119dfdbb44_added_cascade_delete.py | 28 ++++++++++++ backend/src/app/logic/students.py | 22 +++++++--- .../app/routers/editions/students/students.py | 20 ++++++--- backend/src/app/schemas/students.py | 23 ++++++++++ backend/src/database/crud/students.py | 10 ++++- backend/src/database/models.py | 2 +- .../test_database/test_crud/test_students.py | 21 ++++++++- .../test_students/test_students.py | 44 ++++++++++++++++++- 9 files changed, 180 insertions(+), 18 deletions(-) create mode 100644 backend/migrations/versions/32e41fa0af0a_added_to_the_right_side_of_the_.py create mode 100644 backend/migrations/versions/54119dfdbb44_added_cascade_delete.py diff --git a/backend/migrations/versions/32e41fa0af0a_added_to_the_right_side_of_the_.py b/backend/migrations/versions/32e41fa0af0a_added_to_the_right_side_of_the_.py new file mode 100644 index 000000000..96ebdcd6f --- /dev/null +++ b/backend/migrations/versions/32e41fa0af0a_added_to_the_right_side_of_the_.py @@ -0,0 +1,28 @@ +"""added to the right side of the relationship this time + +Revision ID: 32e41fa0af0a +Revises: 54119dfdbb44 +Create Date: 2022-04-10 15:15:05.204273 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '32e41fa0af0a' +down_revision = '54119dfdbb44' +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/54119dfdbb44_added_cascade_delete.py b/backend/migrations/versions/54119dfdbb44_added_cascade_delete.py new file mode 100644 index 000000000..d2514f84d --- /dev/null +++ b/backend/migrations/versions/54119dfdbb44_added_cascade_delete.py @@ -0,0 +1,28 @@ +"""added cascade delete + +Revision ID: 54119dfdbb44 +Revises: 1862d7dea4cc +Create Date: 2022-04-10 15:10:51.251243 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '54119dfdbb44' +down_revision = '1862d7dea4cc' +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/src/app/logic/students.py b/backend/src/app/logic/students.py index a6c903486..414bfc3ac 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -1,10 +1,11 @@ from sqlalchemy.orm import Session +from sqlalchemy.orm.exc import NoResultFound from src.app.schemas.students import NewDecision -from src.database.crud.students import set_definitive_decision_on_student, delete_student, get_students +from src.database.crud.students import set_definitive_decision_on_student, delete_student, get_students, get_emails from src.database.crud.skills import get_skills_by_ids -from src.database.models import Edition, Student, Skill -from src.app.schemas.students import ReturnStudentList, ReturnStudent, CommonQueryParams +from src.database.models import Edition, Student, Skill, DecisionEmail +from src.app.schemas.students import ReturnStudentList, ReturnStudent, CommonQueryParams, ReturnStudentMailList def definitive_decision_on_student(db: Session, student: Student, decision: NewDecision) -> None: @@ -31,6 +32,17 @@ def get_students_search(db: Session, edition: Edition, commons: CommonQueryParam return ReturnStudentList(students=students) -def get_student_return(student: Student) -> ReturnStudent: +def get_student_return(student: Student, edition: Edition) -> ReturnStudent: """return a student""" - return ReturnStudent(student=student) + if student.edition == edition: + return ReturnStudent(student=student) + + raise NoResultFound + + +def get_emails_of_student(db: Session, edition: Edition, student: Student) -> ReturnStudentMailList: + """returns all mails of a student""" + if student.edition != edition: + raise NoResultFound + emails: list[DecisionEmail] = get_emails(db, student) + return ReturnStudentMailList(emails=emails) diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index 754788b4f..64e604449 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -4,8 +4,9 @@ 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 src.app.logic.students import definitive_decision_on_student, remove_student, get_student_return, get_students_search -from src.app.schemas.students import NewDecision, CommonQueryParams, ReturnStudent, ReturnStudentList +from src.app.logic.students import ( + definitive_decision_on_student, remove_student, get_student_return, get_students_search, get_emails_of_student) +from src.app.schemas.students import NewDecision, CommonQueryParams, ReturnStudent, ReturnStudentList, ReturnStudentMailList from src.database.database import get_session from src.database.models import Student, Edition from .suggestions import students_suggestions_router @@ -45,11 +46,13 @@ async def get_student_by_id(edition: Edition = Depends(get_edition), student: St """ Get information about a specific student. """ - return get_student_return(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)): +@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)): """ Make a finalized Yes/Maybe/No decision about a student. @@ -58,9 +61,12 @@ async def make_decision(decision: NewDecision, student: Student = Depends(get_st definitive_decision_on_student(db, student, decision) -@students_router.get("/{student_id}/emails") -async def get_student_email_history(edition: Edition = Depends(get_edition), student: Student = Depends(get_student)): +@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)): """ 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) diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index 83fed943b..d50def337 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -1,3 +1,4 @@ +from datetime import datetime from fastapi import Query from src.app.schemas.webhooks import CamelCaseModel @@ -29,6 +30,7 @@ class Student(CamelCaseModel): skills: list[Skill] class Config: + """Set to ORM mode""" orm_mode = True @@ -57,3 +59,24 @@ def __init__(self, first_name: str = "", last_name: str = "", alumni: bool = Fal self.alumni = alumni self.student_coach = student_coach self.skill_ids = skill_ids + + +class DecisionEmail(CamelCaseModel): + """ + Model to represent DecisionEmail + """ + email_id: int + student_id: int + decision: DecisionEnum + date: datetime + + class Config: + """Set to ORM mode""" + orm_mode = True + + +class ReturnStudentMailList(CamelCaseModel): + """ + Model to return a list of mails of a student + """ + emails: list[DecisionEmail] diff --git a/backend/src/database/crud/students.py b/backend/src/database/crud/students.py index b4475d481..d59f717d8 100644 --- a/backend/src/database/crud/students.py +++ b/backend/src/database/crud/students.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session from src.database.enums import DecisionEnum -from src.database.models import Edition, Skill, Student +from src.database.models import Edition, Skill, Student, DecisionEmail def get_student_by_id(db: Session, student_id: int) -> Student: @@ -22,7 +22,8 @@ def delete_student(db: Session, student: Student) -> None: db.commit() -def get_students(db: Session, edition: Edition, first_name: str = "", last_name: str = "", alumni: bool = False, student_coach: bool = False, skills: list[Skill] = None) -> list[Student]: +def get_students(db: Session, edition: Edition, first_name: str = "", last_name: str = "", alumni: bool = False, + student_coach: bool = False, skills: list[Skill] = None) -> list[Student]: """Get students""" query = db.query(Student)\ .where(Student.edition == edition)\ @@ -42,3 +43,8 @@ def get_students(db: Session, edition: Edition, first_name: str = "", last_name: query = query.where(Student.skills.contains(skill)) return query.all() + + +def get_emails(db: Session, student: Student) -> list[DecisionEmail]: + """Get all emails send to a student""" + return db.query(DecisionEmail).where(DecisionEmail.student_id == student.student_id).all() diff --git a/backend/src/database/models.py b/backend/src/database/models.py index 73d4c0d3d..68a82f730 100644 --- a/backend/src/database/models.py +++ b/backend/src/database/models.py @@ -220,7 +220,7 @@ 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") + 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") diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index 1f0c2441d..2eee76333 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -1,10 +1,11 @@ +import datetime import pytest from sqlalchemy.orm import Session from sqlalchemy.orm.exc import NoResultFound -from src.database.models import Student, User, Edition, Skill +from src.database.models import Student, User, Edition, Skill, DecisionEmail from src.database.enums import DecisionEnum from src.database.crud.students import (get_student_by_id, set_definitive_decision_on_student, - delete_student, get_students) + delete_student, get_students, get_emails) @pytest.fixture @@ -51,6 +52,12 @@ def database_with_data(database_session: Session): database_session.add(student30) database_session.commit() + # DecisionEmail + decision_email: DecisionEmail = DecisionEmail( + student=student01, decision=DecisionEnum.YES, date=datetime.datetime.now()) + database_session.add(decision_email) + database_session.commit() + return database_session @@ -159,3 +166,13 @@ def test_search_students_multiple_skills(database_with_data: Session): Skill).where(Skill.description == "important").all() students = get_students(database_with_data, edition, skills=skills) assert len(students) == 1 + + +def test_get_emails(database_with_data: Session): + """tests to get emails""" + student: Student = get_student_by_id(database_with_data, 1) + emails: list[DecisionEmail] = 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) + assert len(emails) == 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 900c8208c..767d9c8c4 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,8 +1,9 @@ +import datetime; import pytest from sqlalchemy.orm import Session from starlette import status from src.database.enums import DecisionEnum -from src.database.models import Student, Edition, Skill +from src.database.models import Student, Edition, Skill, DecisionEmail from tests.utils.authorization import AuthClient @@ -42,6 +43,12 @@ def database_with_data(database_session: Session) -> Session: database_session.add(student01) database_session.add(student30) database_session.commit() + + # DecisionEmail + decision_email: DecisionEmail = DecisionEmail( + student=student01, decision=DecisionEnum.YES, date=datetime.datetime.now()) + database_session.add(decision_email) + database_session.commit() return database_session @@ -154,6 +161,16 @@ def test_get_student_by_id(database_with_data: Session, auth_client: AuthClient) "/editions/ed2022/students/1").status_code == status.HTTP_200_OK +def test_get_student_by_id_wrong_edition(database_with_data: Session, auth_client: AuthClient): + """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 + + def test_get_students_no_autorization(database_with_data: Session, auth_client: AuthClient): """tests you have to be logged in to get all students""" assert auth_client.get( @@ -256,3 +273,28 @@ def test_get_one_real_one_ghost_skill_students(database_with_data: Session, auth print(response.json()) 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): + """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") + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + +def test_get_emails_student_coach(database_with_data: Session, 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") + assert response.status_code == status.HTTP_403_FORBIDDEN + + +def test_get_emails_student_admin(database_with_data: Session, auth_client: AuthClient): + """tests that an admin can get the mails of a student""" + auth_client.admin() + response = auth_client.get("/editions/ed2022/students/1/emails") + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["emails"]) == 1 + response = auth_client.get("/editions/ed2022/students/2/emails") + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["emails"]) == 0 From be47a54f1374390d4b1355c35101a088ee3c945c Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Sun, 10 Apr 2022 15:48:48 +0200 Subject: [PATCH 339/536] deleted stupid typo --- .../test_routers/test_editions/test_students/test_students.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 767d9c8c4..0e9acfbb2 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,4 +1,4 @@ -import datetime; +import datetime import pytest from sqlalchemy.orm import Session from starlette import status From e5d423a9fb892432d2590dda2331843bf1c86935 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 10 Apr 2022 19:15:14 +0200 Subject: [PATCH 340/536] Begin refactor for pagination --- backend/src/app/logic/users.py | 3 +- backend/src/database/crud/users.py | 7 +- frontend/package.json | 2 + frontend/src/Router.tsx | 4 +- .../AdminsComponents/AdminListItem.tsx | 2 +- .../AdminsComponents/RemoveAdmin.tsx | 2 +- .../UsersComponents/Coaches/Coaches.tsx | 23 +++--- .../Coaches/CoachesComponents/CoachList.tsx | 79 +++++++++---------- .../CoachesComponents/CoachListItem.tsx | 2 +- .../Coaches/CoachesComponents/RemoveCoach.tsx | 2 +- .../UsersComponents/Coaches/styles.ts | 6 ++ .../RequestListItem.tsx | 2 +- frontend/src/utils/api/users/coaches.ts | 8 +- frontend/src/utils/api/users/users.ts | 5 +- .../src/views/AdminsPage/Admins/Admins.tsx | 67 ---------------- frontend/src/views/AdminsPage/Admins/index.ts | 1 - frontend/src/views/AdminsPage/AdminsPage.tsx | 72 ++++++++++++++--- frontend/src/views/AdminsPage/index.ts | 1 + .../views/AdminsPage/{Admins => }/styles.ts | 0 frontend/src/views/UsersPage/UsersPage.tsx | 54 +++++++------ frontend/src/views/index.ts | 1 + frontend/yarn.lock | 14 ++++ 22 files changed, 189 insertions(+), 168 deletions(-) delete mode 100644 frontend/src/views/AdminsPage/Admins/Admins.tsx delete mode 100644 frontend/src/views/AdminsPage/Admins/index.ts create mode 100644 frontend/src/views/AdminsPage/index.ts rename frontend/src/views/AdminsPage/{Admins => }/styles.ts (100%) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index 1613692cd..d73efc5bc 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -78,7 +78,8 @@ def get_request_list(db: Session, edition_name: str | None, page: int) -> UserRe requests_model = [] for request in requests: - user_req = UserRequest(request_id=request.request_id, edition_name=request.edition.name, user=request.user) + user_req = UserRequest(request_id=request.request_id, edition_name=request.edition.name, + user=user_model_to_schema(request.user)) requests_model.append(user_req) return UserRequestsResponse(requests=requests_model) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 2a2db0d94..215335439 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -140,7 +140,12 @@ def get_requests_page(db: Session, page: int) -> list[CoachRequest]: def _get_requests_for_edition_query(db: Session, edition: Edition) -> Query: - return db.query(CoachRequest).where(CoachRequest.edition_id == edition.edition_id).join(User) + return db.query(CoachRequest)\ + .where(CoachRequest.edition_id == edition.edition_id)\ + .join(User)\ + .join(AuthEmail, isouter=True) \ + .join(AuthGitHub, isouter=True) \ + .join(AuthGoogle, isouter=True) def get_requests_for_edition(db: Session, edition_name: str) -> list[CoachRequest]: diff --git a/frontend/package.json b/frontend/package.json index 3ed53efdd..63c4f6762 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,6 +6,7 @@ "@fortawesome/fontawesome-svg-core": "^1.3.0", "@fortawesome/free-solid-svg-icons": "^6.0.0", "@fortawesome/react-fontawesome": "^0.1.17", + "@types/react-infinite-scroller": "^1.2.3", "axios": "^0.26.1", "bootstrap": "5.1.3", "react": "^17.0.2", @@ -14,6 +15,7 @@ "react-collapsible": "^2.8.4", "react-dom": "^17.0.2", "react-icons": "^4.3.1", + "react-infinite-scroller": "^1.2.5", "react-router-bootstrap": "^0.26.1", "react-router-dom": "^6.2.1", "react-scripts": "^5.0.0", diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 368bfaaea..a24209143 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -10,6 +10,7 @@ import { RegisterPage, StudentsPage, UsersPage, + AdminsPage, VerifyingTokenPage, } from "./views"; import { ForbiddenPage, NotFoundPage } from "./views/errors"; @@ -39,8 +40,7 @@ export default function Router() { } /> } /> }> - {/* TODO admins page */} - } /> + } /> }> {/* TODO editions page */} diff --git a/frontend/src/components/AdminsComponents/AdminListItem.tsx b/frontend/src/components/AdminsComponents/AdminListItem.tsx index 90997a5db..0cd4d3f62 100644 --- a/frontend/src/components/AdminsComponents/AdminListItem.tsx +++ b/frontend/src/components/AdminsComponents/AdminListItem.tsx @@ -11,7 +11,7 @@ export default function AdminItem(props: { admin: User; refresh: () => void }) { return ( {props.admin.name} - {props.admin.email} + {props.admin.auth.email} diff --git a/frontend/src/components/AdminsComponents/RemoveAdmin.tsx b/frontend/src/components/AdminsComponents/RemoveAdmin.tsx index fdb14b6f0..ecedc3a1f 100644 --- a/frontend/src/components/AdminsComponents/RemoveAdmin.tsx +++ b/frontend/src/components/AdminsComponents/RemoveAdmin.tsx @@ -52,7 +52,7 @@ export default function RemoveAdmin(props: { admin: User; refresh: () => void })

                                        {props.admin.name}

                                        -

                                        {props.admin.email}

                                        +

                                        {props.admin.auth.email}

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

                                        diff --git a/frontend/src/components/UsersComponents/Coaches/Coaches.tsx b/frontend/src/components/UsersComponents/Coaches/Coaches.tsx index bc02a7519..e3f084681 100644 --- a/frontend/src/components/UsersComponents/Coaches/Coaches.tsx +++ b/frontend/src/components/UsersComponents/Coaches/Coaches.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { CoachesTitle, CoachesContainer } from "./styles"; import { User } from "../../../utils/api/users/users"; import { Error, SearchInput } from "../PendingRequests/styles"; @@ -11,6 +11,7 @@ import { CoachList, AddCoach } from "./CoachesComponents"; * @param props.allCoaches The list of all coaches of the current edition. * @param props.users A list of all users who can be added as coach. * @param props.refresh A function which will be called when a coach is added/removed. + * @param props.getMoreCoaches A function to load more coaches. * @param props.gotData All data is received. * @param props.gettingData Data is not available yet. * @param props.error An error message. @@ -20,23 +21,15 @@ export default function Coaches(props: { allCoaches: User[]; users: User[]; refresh: () => void; + getMoreCoaches: (page: number) => void; gotData: boolean; gettingData: boolean; error: string; + moreCoachesAvailable: boolean; }) { - const [coaches, setCoaches] = useState([]); // All coaches after filter + // const [coaches, setCoaches] = useState([]); // All coaches after filter const [searchTerm, setSearchTerm] = useState(""); // The word set in filter - useEffect(() => { - const newCoaches: User[] = []; - for (const coach of props.allCoaches) { - if (coach.name.toUpperCase().includes(searchTerm.toUpperCase())) { - newCoaches.push(coach); - } - } - setCoaches(newCoaches); - }, [props.allCoaches, searchTerm]); - /** * Apply a filter to the coach list. * Only keep coaches who's name contain the searchterm. @@ -50,7 +43,7 @@ export default function Coaches(props: { newCoaches.push(coach); } } - setCoaches(newCoaches); + // setCoaches(newCoaches); }; return ( @@ -59,11 +52,13 @@ export default function Coaches(props: { filter(e.target.value)} /> {props.error} diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx index 3e1dc738b..6338399d7 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx @@ -1,8 +1,7 @@ import { User } from "../../../../utils/api/users/users"; -import { SpinnerContainer } from "../../PendingRequests/styles"; -import { Spinner } from "react-bootstrap"; -import { CoachesTable, RemoveTh } from "../styles"; +import { CoachesTable, ListDiv, RemoveTh } from "../styles"; import React from "react"; +import InfiniteScroll from "react-infinite-scroller"; import { CoachListItem } from "./index"; /** @@ -12,6 +11,7 @@ import { CoachListItem } from "./index"; * @param props.edition The edition. * @param props.gotData All data is received. * @param props.refresh A function which will be called when a coach is removed. + * @param props.getMoreCoaches A function to load more coaches. */ export default function CoachList(props: { coaches: User[]; @@ -19,44 +19,43 @@ export default function CoachList(props: { edition: string; gotData: boolean; refresh: () => void; + getMoreCoaches: (page: number) => void; + moreCoachesAvailable: boolean; }) { - if (props.loading) { - return ( - - - - ); - } else if (props.coaches.length === 0) { - if (props.gotData) { - return
                                        No coaches for this edition
                                        ; - } else { - return null; - } - } - - const body = ( - - {props.coaches.map(coach => ( - - ))} - - ); - return ( - - - - Name - Email - Remove from edition - - - {body} - + + + Loading ... +
                                        + } + useWindow={true} + initialLoad={true} + > + + + + Name + Email + Remove from edition + + + + {props.coaches.map(coach => ( + + ))} + + + + ); } diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx index 821e9ff56..b22c08809 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx @@ -18,7 +18,7 @@ export default function CoachListItem(props: { return ( {props.coach.name} - {props.coach.email} + {props.coach.auth.email} diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx index 49a8b6857..67fd58a9c 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx @@ -58,7 +58,7 @@ export default function RemoveCoach(props: { coach: User; edition: string; refre

                                        {props.coach.name}

                                        - {props.coach.email} + {props.coach.auth.email}
                                        + + + } - useWindow={true} - initialLoad={true} + useWindow={false} + initialLoad={false} > diff --git a/frontend/src/components/UsersComponents/Coaches/styles.ts b/frontend/src/components/UsersComponents/Coaches/styles.ts index b17fc854e..d5eecceb8 100644 --- a/frontend/src/components/UsersComponents/Coaches/styles.ts +++ b/frontend/src/components/UsersComponents/Coaches/styles.ts @@ -33,6 +33,8 @@ export const RemoveTd = styled.td` `; export const ListDiv = styled.div` - height: 500px; + width: 100%; + height: 400px; overflow: auto; + margin-top: 10px; `; diff --git a/frontend/src/utils/api/users/coaches.ts b/frontend/src/utils/api/users/coaches.ts index df3341a99..2e952db67 100644 --- a/frontend/src/utils/api/users/coaches.ts +++ b/frontend/src/utils/api/users/coaches.ts @@ -4,11 +4,21 @@ import { axiosInstance } from "../api"; /** * Get all coaches from the given edition * @param edition The edition name + * @param name The username to filter * @param page */ -export async function getCoaches(edition: string, page: number): Promise { +export async function getCoaches(edition: string, name: string, page: number): Promise { + // eslint-disable-next-line promise/param-names + // await new Promise(r => setTimeout(r, 2000)); + if (name) { + const response = await axiosInstance.get( + `/users/?edition=${edition}&page=${page}&name=${name}` + ); + // console.log(`|page: ${page} Search:${name} Found: ${response.data.users.length}`); + return response.data as UsersList; + } const response = await axiosInstance.get(`/users/?edition=${edition}&page=${page}`); - console.log(`got ${page}: ${response.data.users.length}`); + // console.log(`|page: ${page} Search:${name} Found: ${response.data.users.length}`); return response.data as UsersList; } diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index 8e3093b1f..650fd3d34 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -1,47 +1,51 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { UsersPageDiv, AdminsButton, UsersHeader } from "./styles"; import { Coaches } from "../../components/UsersComponents/Coaches"; import { InviteUser } from "../../components/UsersComponents/InviteUser"; import { PendingRequests } from "../../components/UsersComponents/PendingRequests"; -import { User } from "../../utils/api/users/users"; +import { getUsers, User } from "../../utils/api/users/users"; import { getCoaches } from "../../utils/api/users/coaches"; /** * Page for admins to manage coach and admin settings. */ function UsersPage() { - const [allCoaches, setAllCoaches] = useState([]); // All coaches from the edition + const [coaches, setCoaches] = useState([]); // All coaches from the edition const [users, setUsers] = useState([]); // All users which are not a coach const [gettingData, setGettingData] = useState(false); // Waiting for data const [gotData, setGotData] = useState(false); // Received data const [error, setError] = useState(""); // Error message const [moreCoachesAvailable, setMoreCoachesAvailable] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); // The word set in filter const params = useParams(); const navigate = useNavigate(); - async function getCoachesData(page: number) { + async function getCoachesData(page: number, filter: string | undefined = undefined) { + if (filter === undefined) { + filter = searchTerm; + } setGettingData(true); - setGotData(false); setError(""); try { - const coachResponse = await getCoaches(params.editionId as string, page); - if (coachResponse.users.length === 0) { + const coachResponse = await getCoaches(params.editionId as string, filter, page); + if (coachResponse.users.length !== 25) { setMoreCoachesAvailable(false); + } + if (page === 0) { + setCoaches(coachResponse.users); } else { - console.log(allCoaches.length); - setAllCoaches(allCoaches.concat(coachResponse.users)); - console.log(allCoaches.concat(coachResponse.users).length); + setCoaches(coaches.concat(coachResponse.users)); } - // const usersResponse = await getUsers(); + const usersResponse = await getUsers(); const users: User[] = []; - // for (const user of usersResponse.users) { - // if (!coachResponse.users.some(e => e.userId === user.userId)) { - // users.push(user); - // } - // } + for (const user of usersResponse.users) { + if (!coachResponse.users.some(e => e.userId === user.userId)) { + users.push(user); + } + } setUsers(users); setGotData(true); @@ -52,6 +56,21 @@ function UsersPage() { } } + useEffect(() => { + if (!gotData && !gettingData && !error) { + getCoachesData(0); + } + }); + + function filterCoachesData(searchTerm: string) { + setGettingData(true); + setGotData(false); + setSearchTerm(searchTerm); + setCoaches([]); + setMoreCoachesAvailable(true); + getCoachesData(0, searchTerm); + } + if (params.editionId === undefined) { return
                                        Error
                                        ; } else { @@ -70,14 +89,16 @@ function UsersPage() { /> getCoachesData(0)} gotData={gotData} gettingData={gettingData} error={error} getMoreCoaches={getCoachesData} + searchCoaches={filterCoachesData} moreCoachesAvailable={moreCoachesAvailable} + searchTerm={searchTerm} /> ); From ce3ce83c712d9341289c74c0079f18c62ace8b02 Mon Sep 17 00:00:00 2001 From: beguille Date: Mon, 11 Apr 2022 13:35:23 +0200 Subject: [PATCH 342/536] combined get_edition and check_latest_edition into one --- .../src/app/routers/editions/invites/invites.py | 6 +++--- .../src/app/routers/editions/projects/projects.py | 10 +++++----- .../projects/students/projects_students.py | 15 +++++++-------- .../src/app/routers/editions/register/register.py | 7 ++++--- .../src/app/routers/editions/webhooks/webhooks.py | 6 +++--- backend/src/app/utils/dependencies.py | 4 ++-- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/backend/src/app/routers/editions/invites/invites.py b/backend/src/app/routers/editions/invites/invites.py index 46021cee5..2ca3e20bd 100644 --- a/backend/src/app/routers/editions/invites/invites.py +++ b/backend/src/app/routers/editions/invites/invites.py @@ -6,7 +6,7 @@ from src.app.logic.invites import create_mailto_link, delete_invite_link, get_pending_invites_list from src.app.routers.tags import Tags from src.app.schemas.invites import InvitesListResponse, EmailAddress, NewInviteLink, InviteLink as InviteLinkModel -from src.app.utils.dependencies import get_edition, get_invite_link, require_admin, check_latest_edition +from src.app.utils.dependencies import get_edition, get_invite_link, require_admin, get_latest_edition from src.database.database import get_session from src.database.models import Edition, InviteLink as InviteLinkDB @@ -22,9 +22,9 @@ async def get_invites(db: Session = Depends(get_session), edition: Edition = Dep @invites_router.post("/", status_code=status.HTTP_201_CREATED, response_model=NewInviteLink, - dependencies=[Depends(require_admin), Depends(check_latest_edition)]) + dependencies=[Depends(require_admin)]) async def create_invite(email: EmailAddress, db: Session = Depends(get_session), - edition: Edition = Depends(get_edition)): + edition: Edition = Depends(get_latest_edition)): """ Create a new invitation link for the current edition. """ diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index 0faf27711..6167a8f97 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -8,7 +8,7 @@ from src.app.routers.tags import Tags from src.app.schemas.projects import ProjectList, Project, InputProject, \ ConflictStudentList -from src.app.utils.dependencies import get_edition, get_project, require_admin, require_coach, check_latest_edition +from src.app.utils.dependencies import get_edition, get_project, require_admin, require_coach, get_latest_edition from src.database.database import get_session from src.database.models import Edition, Project as ProjectModel from .students import project_students_router @@ -26,9 +26,9 @@ async def get_projects(db: Session = Depends(get_session), edition: Edition = De @projects_router.post("/", status_code=status.HTTP_201_CREATED, response_model=Project, - dependencies=[Depends(require_admin), Depends(check_latest_edition)]) + dependencies=[Depends(require_admin)]) async def create_project(input_project: InputProject, - db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): + db: Session = Depends(get_session), edition: Edition = Depends(get_latest_edition)): """ Create a new project """ @@ -46,7 +46,7 @@ async def get_conflicts(db: Session = Depends(get_session), edition: Edition = D @projects_router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, - dependencies=[Depends(require_admin), Depends(check_latest_edition)]) + dependencies=[Depends(require_admin), Depends(get_latest_edition)]) async def delete_project(project_id: int, db: Session = Depends(get_session)): """ Delete a specific project. @@ -68,7 +68,7 @@ async def get_project_route(project: ProjectModel = Depends(get_project)): @projects_router.patch("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, - dependencies=[Depends(require_admin), Depends(check_latest_edition)]) + dependencies=[Depends(require_admin), Depends(get_latest_edition)]) async def patch_project(project_id: int, input_project: InputProject, db: Session = Depends(get_session)): """ Update a project, changing some fields. 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 8f22d51f3..0edc42d02 100644 --- a/backend/src/app/routers/editions/projects/students/projects_students.py +++ b/backend/src/app/routers/editions/projects/students/projects_students.py @@ -7,15 +7,15 @@ logic_change_project_role, logic_confirm_project_role 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_edition, check_latest_edition +from src.app.utils.dependencies import get_project, require_admin, require_coach, get_latest_edition from src.database.database import get_session -from src.database.models import Project, User, Edition +from src.database.models import Project, User 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), Depends(check_latest_edition)]) + dependencies=[Depends(require_coach), Depends(get_latest_edition)]) async def remove_student_from_project(student_id: int, db: Session = Depends(get_session), project: Project = Depends(get_project)): """ @@ -25,7 +25,7 @@ async def remove_student_from_project(student_id: int, db: Session = Depends(get @project_students_router.patch("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, - dependencies=[Depends(check_latest_edition)]) + 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)): """ @@ -35,10 +35,9 @@ async def change_project_role(student_id: int, input_sr: InputStudentRole, db: S @project_students_router.post("/{student_id}", status_code=status.HTTP_201_CREATED, response_class=Response, - dependencies=[Depends(check_latest_edition)]) + 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), - edition: Edition = Depends(get_edition)): + project: Project = Depends(get_project), user: User = Depends(require_coach)): """ Add a student to a project. @@ -48,7 +47,7 @@ async def add_student_to_project(student_id: int, input_sr: InputStudentRole, db @project_students_router.post("/{student_id}/confirm", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, - dependencies=[Depends(require_admin)]) + 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)): """ diff --git a/backend/src/app/routers/editions/register/register.py b/backend/src/app/routers/editions/register/register.py index ccac62e83..e008c13b6 100644 --- a/backend/src/app/routers/editions/register/register.py +++ b/backend/src/app/routers/editions/register/register.py @@ -5,15 +5,16 @@ from src.app.logic.register import create_request from src.app.routers.tags import Tags from src.app.schemas.register import NewUser -from src.app.utils.dependencies import get_edition, check_latest_edition +from src.app.utils.dependencies import get_latest_edition from src.database.database import get_session from src.database.models import Edition registration_router = APIRouter(prefix="/register", tags=[Tags.REGISTRATION]) -@registration_router.post("/email", status_code=status.HTTP_201_CREATED, dependencies=[Depends(check_latest_edition)]) -async def register_email(user: NewUser, db: Session = Depends(get_session), edition: Edition = Depends(get_edition)): +@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)): """ Register a new account using the email/password format. """ diff --git a/backend/src/app/routers/editions/webhooks/webhooks.py b/backend/src/app/routers/editions/webhooks/webhooks.py index 5f0d4a9e8..9a687aa7f 100644 --- a/backend/src/app/routers/editions/webhooks/webhooks.py +++ b/backend/src/app/routers/editions/webhooks/webhooks.py @@ -5,7 +5,7 @@ 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, check_latest_edition +from src.app.utils.dependencies import get_edition, require_admin, get_latest_edition from src.database.crud.webhooks import get_webhook, create_webhook from src.database.database import get_session from src.database.models import Edition @@ -19,8 +19,8 @@ def valid_uuid(uuid: str, database: Session = Depends(get_session)): @webhooks_router.post("/", response_model=WebhookUrlResponse, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(require_admin), Depends(check_latest_edition)]) -def new(edition: Edition = Depends(get_edition), database: Session = Depends(get_session)): + dependencies=[Depends(require_admin)]) +def new(edition: Edition = Depends(get_latest_edition), database: Session = Depends(get_session)): """Create a new webhook for an edition""" return create_webhook(database, edition) diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index 1133f502b..ca860d912 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -21,8 +21,8 @@ def get_edition(edition_name: str, database: Session = Depends(get_session)) -> return get_edition_by_name(database, edition_name) -def check_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)""" +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: raise ReadOnlyEditionException From f84a5eff86495aa3898e69a65589685af069fa95 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Mon, 11 Apr 2022 13:47:15 +0200 Subject: [PATCH 343/536] Requestlist pagination + infinite scroll --- .../PendingRequests/PendingRequests.tsx | 53 +++++++++-------- .../PendingRequestsComponents/RequestList.tsx | 59 ++++++++++++------- frontend/src/utils/api/users/requests.ts | 17 +++++- 3 files changed, 82 insertions(+), 47 deletions(-) diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx index 4d2b02375..b4f7bde25 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from "react"; import Collapsible from "react-collapsible"; -import { PendingRequestsContainer, Error } from "./styles"; +import { PendingRequestsContainer, Error, SearchInput } from "./styles"; import { getRequests, Request } from "../../../utils/api/users/requests"; -import { RequestFilter, RequestList, RequestsHeader } from "./PendingRequestsComponents"; +import { RequestList, RequestsHeader } from "./PendingRequestsComponents"; /** * A collapsible component which contains all coach requests for a given edition. @@ -10,26 +10,37 @@ import { RequestFilter, RequestList, RequestsHeader } from "./PendingRequestsCom * @param props.edition The edition. */ export default function PendingRequests(props: { edition: string; refreshCoaches: () => void }) { - const [allRequests, setAllRequests] = useState([]); // All requests for the given edition const [requests, setRequests] = useState([]); // All requests after filter const [gettingRequests, setGettingRequests] = useState(false); // Waiting for data const [searchTerm, setSearchTerm] = useState(""); // The word set in filter const [gotData, setGotData] = useState(false); // Received data const [open, setOpen] = useState(false); // Collapsible is open const [error, setError] = useState(""); // Error message + const [moreRequestsAvailable, setMoreRequestsAvailable] = useState(true); function refresh(coachAdded: boolean) { - getData(); + // TODO + getData(0); if (coachAdded) { props.refreshCoaches(); } } - async function getData() { + async function getData(page: number, filter: string | undefined = undefined) { + if (filter === undefined) { + filter = searchTerm; + } try { - const response = await getRequests(props.edition); - setAllRequests(response.requests); - setRequests(response.requests); + const response = await getRequests(props.edition, filter, page); + if (response.requests.length !== 25) { + setMoreRequestsAvailable(false); + } + if (page === 0) { + setRequests(response.requests); + } else { + setRequests(requests.concat(response.requests)); + } + setGotData(true); setGettingRequests(false); } catch (exception) { @@ -41,19 +52,17 @@ export default function PendingRequests(props: { edition: string; refreshCoaches useEffect(() => { if (!gotData && !gettingRequests && !error) { setGettingRequests(true); - getData(); + getData(0); } }, [gotData, gettingRequests, error, getData]); - const filter = (word: string) => { - setSearchTerm(word); - const newRequests: Request[] = []; - for (const request of allRequests) { - if (request.user.name.toUpperCase().includes(word.toUpperCase())) { - newRequests.push(request); - } - } - setRequests(newRequests); + const searchRequests = (searchTerm: string) => { + setGettingRequests(true); + setGotData(false); + setSearchTerm(searchTerm); + setRequests([]); + setMoreRequestsAvailable(true); + getData(0, searchTerm); }; return ( @@ -63,16 +72,14 @@ export default function PendingRequests(props: { edition: string; refreshCoaches onOpening={() => setOpen(true)} onClosing={() => setOpen(false)} > - filter(word)} - show={allRequests.length > 0} - /> + searchRequests(e.target.value)} /> {error} diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx index a1fe80f20..09c74c101 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx +++ b/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx @@ -3,6 +3,8 @@ import { AcceptRejectTh, RequestsTable, SpinnerContainer } from "../styles"; import { Spinner } from "react-bootstrap"; import React from "react"; import RequestListItem from "./RequestListItem"; +import InfiniteScroll from "react-infinite-scroller"; +import { ListDiv } from "../../Coaches/styles"; /** * A list of [[RequestListItem]]s. @@ -16,6 +18,8 @@ export default function RequestList(props: { loading: boolean; gotData: boolean; refresh: (coachAdded: boolean) => void; + moreRequestAvailable: boolean; + getMoreRequests: (page: number) => void; }) { if (props.loading) { return ( @@ -31,28 +35,39 @@ export default function RequestList(props: { } } - const body = ( - - {props.requests.map(request => ( - - ))} - - ); - return ( - - - - Name - Email - Accept/Reject - - - {body} - + + + + + } + useWindow={false} + initialLoad={false} + > + + + + Name + Email + Accept/Reject + + + + {props.requests.map(request => ( + + ))} + + + + ); } diff --git a/frontend/src/utils/api/users/requests.ts b/frontend/src/utils/api/users/requests.ts index c0225b36e..d9d92b9d5 100644 --- a/frontend/src/utils/api/users/requests.ts +++ b/frontend/src/utils/api/users/requests.ts @@ -20,8 +20,21 @@ export interface GetRequestsResponse { * Get all pending requests of a given edition * @param {string} edition The edition's name */ -export async function getRequests(edition: string): Promise { - const response = await axiosInstance.get(`/users/requests?edition=${edition}`); +export async function getRequests( + edition: string, + name: string, + page: number +): Promise { + if (name) { + const response = await axiosInstance.get( + `/users/requests?edition=${edition}&page=${page}&name=${name}` + ); + console.log(`|page: ${page} Search:${name} Found: ${response.data.requests.length}`); + console.log(response.data.requests); + return response.data as GetRequestsResponse; + } + const response = await axiosInstance.get(`/users/requests?edition=${edition}&page=${page}`); + console.log(`|page: ${page} Search:${name} Found: ${response.data.requests.length}`); return response.data as GetRequestsResponse; } From 725b4cf89acc44bf8af477e878f776af8264f7c5 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Mon, 11 Apr 2022 14:20:40 +0200 Subject: [PATCH 344/536] Add name filter for GET users --- backend/src/app/logic/users.py | 10 +++-- backend/src/app/routers/users/users.py | 8 +++- backend/src/database/crud/users.py | 31 +++++++++---- .../test_database/test_crud/test_users.py | 38 ++++++++++++++++ .../test_routers/test_users/test_users.py | 44 +++++++++++++++++++ 5 files changed, 117 insertions(+), 14 deletions(-) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index 1613692cd..d17132800 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -66,15 +66,19 @@ def remove_coach_all_editions(db: Session, user_id: int): users_crud.remove_coach_all_editions(db, user_id) -def get_request_list(db: Session, edition_name: str | None, page: int) -> UserRequestsResponse: +def get_request_list(db: Session, 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 """ + + if user_name is None: + user_name = "" + if edition_name is None: - requests = users_crud.get_requests_page(db, page) + requests = users_crud.get_requests_page(db, page, user_name) else: - requests = users_crud.get_requests_for_edition_page(db, edition_name, page) + requests = users_crud.get_requests_for_edition_page(db, edition_name, page, user_name) requests_model = [] for request in requests: diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index 9ae446189..df2afe577 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -70,11 +70,15 @@ async def remove_from_all_editions(user_id: int, db: Session = Depends(get_sessi @users_router.get("/requests", response_model=UserRequestsResponse, dependencies=[Depends(require_admin)]) -async def get_requests(edition: str | None = Query(None), page: int = 0, db: Session = Depends(get_session)): +async def get_requests( + edition: str | None = Query(None), + user: str | None = Query(None), + page: int = 0, + db: Session = Depends(get_session)): """ Get pending userrequests """ - return logic.get_request_list(db, edition, page) + return logic.get_request_list(db, edition, user, page) @users_router.post("/requests/{request_id}/accept", status_code=204, dependencies=[Depends(require_admin)]) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index ad63dee92..199451cc1 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -121,8 +121,8 @@ def remove_coach_all_editions(db: Session, user_id: int): db.commit() -def _get_requests_query(db: Session) -> Query: - return db.query(CoachRequest).join(User) +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(db: Session) -> list[CoachRequest]: @@ -132,29 +132,40 @@ def get_requests(db: Session) -> list[CoachRequest]: return _get_requests_query(db).all() -def get_requests_page(db: Session, page: int) -> list[CoachRequest]: +def get_requests_page(db: Session, page: int, user_name: str = "") -> list[CoachRequest]: """ Get all userrequests """ - return paginate(_get_requests_query(db), page).all() + return paginate(_get_requests_query(db, user_name), page).all() -def _get_requests_for_edition_query(db: Session, edition: Edition) -> Query: - return db.query(CoachRequest).where(CoachRequest.edition_id == edition.edition_id).join(User) +def _get_requests_for_edition_query(db: Session, edition: Edition, user_name: str = "") -> Query: + return db.query(CoachRequest)\ + .where(CoachRequest.edition_id == edition.edition_id)\ + .join(User)\ + .where(User.name.contains(user_name))\ + .join(AuthEmail, isouter=True)\ + .join(AuthGitHub, isouter=True)\ + .join(AuthGoogle, isouter=True) -def get_requests_for_edition(db: Session, edition_name: str) -> list[CoachRequest]: +def get_requests_for_edition(db: Session, 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() -def get_requests_for_edition_page(db: Session, edition_name: str, page: int) -> list[CoachRequest]: +def get_requests_for_edition_page( + db: Session, + edition_name: str, + page: int, + user_name: str = "" +) -> list[CoachRequest]: """ Get all userrequests from a given edition """ - return paginate(_get_requests_for_edition_query(db, get_edition_by_name(db, edition_name)), page).all() + return paginate(_get_requests_for_edition_query(db, get_edition_by_name(db, edition_name), user_name), page).all() def accept_request(db: Session, request_id: int): @@ -165,6 +176,7 @@ def accept_request(db: Session, request_id: int): 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() def reject_request(db: Session, request_id: int): @@ -172,3 +184,4 @@ def reject_request(db: Session, request_id: int): Remove request """ db.query(CoachRequest).where(CoachRequest.request_id == request_id).delete() + db.commit() diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index f10c515c8..d6f193717 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -378,6 +378,25 @@ def test_get_requests_paginated(database_session: Session): ) - DB_PAGE_SIZE +def test_get_requests_paginated_filter_user_name(database_session: Session): + edition = models.Edition(year=2022, name="ed2022") + database_session.add(edition) + + count = 0 + for i in range(round(DB_PAGE_SIZE * 1.5)): + user = models.User(name=f"User {i}", admin=False) + database_session.add(user) + database_session.add(CoachRequest(user=user, edition=edition)) + if "1" in str(i): + count += 1 + database_session.commit() + + assert len(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")) == \ + max(count-DB_PAGE_SIZE, 0) + + def test_get_all_requests_from_edition(database_session: Session): """Test get request for all userrequests of a given edition""" @@ -428,6 +447,25 @@ def test_get_requests_for_edition_paginated(database_session: Session): ) - DB_PAGE_SIZE +def test_get_requests_for_edition_paginated_filter_user_name(database_session: Session): + edition = models.Edition(year=2022, name="ed2022") + database_session.add(edition) + + count = 0 + for i in range(round(DB_PAGE_SIZE * 1.5)): + user = models.User(name=f"User {i}", admin=False) + database_session.add(user) + database_session.add(CoachRequest(user=user, edition=edition)) + if "1" in str(i): + count += 1 + database_session.commit() + + assert len(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")) == \ + max(count-DB_PAGE_SIZE, 0) + + def test_accept_request(database_session: Session): """Test accepting a coach request""" diff --git a/backend/tests/test_routers/test_users/test_users.py b/backend/tests/test_routers/test_users/test_users.py index 59c799e7f..4729abae7 100644 --- a/backend/tests/test_routers/test_users/test_users.py +++ b/backend/tests/test_routers/test_users/test_users.py @@ -381,6 +381,28 @@ def test_get_all_requests_paginated(database_session: Session, auth_client: Auth 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): + """Test endpoint for getting a paginated list of requests""" + edition = models.Edition(year=2022, name="ed2022") + + count = 0 + for i in range(round(DB_PAGE_SIZE * 1.5)): + user = models.User(name=f"User {i}", admin=False) + database_session.add(user) + database_session.add(models.CoachRequest(user=user, edition=edition)) + if "1" in str(i): + count += 1 + 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) + + def test_get_all_requests_from_edition(database_session: Session, auth_client: AuthClient): """Test endpoint for getting all userrequests of a given edition""" auth_client.admin() @@ -439,6 +461,28 @@ def test_get_all_requests_for_edition_paginated(database_session: Session, auth_ 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): + """Test endpoint for getting a paginated list of requests""" + edition = models.Edition(year=2022, name="ed2022") + + count = 0 + for i in range(round(DB_PAGE_SIZE * 1.5)): + user = models.User(name=f"User {i}", admin=False) + database_session.add(user) + database_session.add(models.CoachRequest(user=user, edition=edition)) + if "1" in str(i): + count += 1 + 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) + + def test_accept_request(database_session, auth_client: AuthClient): """Test endpoint for accepting a coach request""" auth_client.admin() From 0d2bf3b02bac185fd23bd4213abdda6b26229827 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 11 Apr 2022 14:40:14 +0200 Subject: [PATCH 345/536] Persist edition in sessionstorage --- .../src/components/Navbar/EditionDropdown.tsx | 9 +++---- frontend/src/components/Navbar/Navbar.tsx | 23 ++++++++++-------- frontend/src/data/enums/index.ts | 3 ++- frontend/src/data/enums/local-storage.ts | 2 +- frontend/src/data/enums/session-storage.ts | 6 +++++ frontend/src/utils/local-storage/auth.ts | 24 +++++++++---------- .../utils/session-storage/current-edition.ts | 20 ++++++++++++++++ 7 files changed, 59 insertions(+), 28 deletions(-) create mode 100644 frontend/src/data/enums/session-storage.ts create mode 100644 frontend/src/utils/session-storage/current-edition.ts diff --git a/frontend/src/components/Navbar/EditionDropdown.tsx b/frontend/src/components/Navbar/EditionDropdown.tsx index 8d9fc79fb..6d9ef64fb 100644 --- a/frontend/src/components/Navbar/EditionDropdown.tsx +++ b/frontend/src/components/Navbar/EditionDropdown.tsx @@ -2,11 +2,10 @@ import React from "react"; import NavDropdown from "react-bootstrap/NavDropdown"; import { StyledDropdownItem } from "./styles"; import { useNavigate } from "react-router-dom"; +import { getCurrentEdition } from "../../utils/session-storage/current-edition"; interface Props { editions: string[]; - currentEdition: string; - setCurrentEdition: (edition: string) => void; } /** @@ -16,6 +15,8 @@ export default function EditionDropdown(props: Props) { const navItems: React.ReactNode[] = []; const navigate = useNavigate(); + const currentEdition = getCurrentEdition(); + /** * Change the route based on the edition * This can't be a separate function because it uses hooks which may @@ -32,7 +33,7 @@ export default function EditionDropdown(props: Props) { navItems.push( handleSelect(edition)} > {edition} @@ -40,5 +41,5 @@ export default function EditionDropdown(props: Props) { ); }); - return {navItems}; + return {navItems}; } diff --git a/frontend/src/components/Navbar/Navbar.tsx b/frontend/src/components/Navbar/Navbar.tsx index 75b736178..9e13bd86f 100644 --- a/frontend/src/components/Navbar/Navbar.tsx +++ b/frontend/src/components/Navbar/Navbar.tsx @@ -3,19 +3,26 @@ import { BSNavbar, StyledDropdownItem } from "./styles"; import { useAuth } from "../../contexts"; import Brand from "./Brand"; import Nav from "react-bootstrap/Nav"; -import { useEffect, useState } from "react"; import NavDropdown from "react-bootstrap/NavDropdown"; import EditionDropdown from "./EditionDropdown"; import "./Navbar.css"; import LogoutButton from "./LogoutButton"; +import { getCurrentEdition, setCurrentEdition } from "../../utils/session-storage/current-edition"; +import { useParams } from "react-router-dom"; export default function Navbar() { const { isLoggedIn, editions } = useAuth(); - const [currentEdition, setCurrentEdition] = useState(editions[0]); + const params = useParams(); - useEffect(() => { - setCurrentEdition(editions[0]); - }, [editions]); + // 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 = params.editionId ? params.editionId : getCurrentEdition() || editions[0]; + + // Set the value of the new edition in SessionStorage if useful + if (currentEdition) { + setCurrentEdition(currentEdition); + } // Don't render Navbar if not logged in if (!isLoggedIn) { @@ -30,11 +37,7 @@ export default function Navbar() {
                                      - getCoachesData(0)} - /> + Date: Mon, 11 Apr 2022 16:12:55 +0200 Subject: [PATCH 348/536] Remove edition from storage when logging out --- frontend/src/contexts/auth-context.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx index 2817b371d..5b0f8a8cb 100644 --- a/frontend/src/contexts/auth-context.tsx +++ b/frontend/src/contexts/auth-context.tsx @@ -4,6 +4,7 @@ import React, { useContext, ReactNode, useState } from "react"; import { getToken, setToken as setTokenInStorage } from "../utils/local-storage"; import { User } from "../data/interfaces"; import { setBearerToken } from "../utils/api"; +import { setCurrentEdition } from "../utils/session-storage/current-edition"; /** * Interface that holds the data stored in the AuthContext. @@ -114,4 +115,7 @@ export function logOut(authContext: AuthContextState) { authContext.setRole(null); authContext.setEditions([]); authContext.setToken(null); + + // Remove current edition from SessionStorage + setCurrentEdition(null); } From 9bb8c5046e88d94f45c6b58d13e0c496a6a7fc80 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Mon, 11 Apr 2022 19:49:47 +0200 Subject: [PATCH 349/536] Add exclude_edition filter for GET users --- backend/src/app/logic/users.py | 14 ++- backend/src/app/routers/users/users.py | 5 +- backend/src/database/crud/users.py | 51 +++++++-- .../test_database/test_crud/test_users.py | 107 ++++++++++++++++- .../test_routers/test_users/test_users.py | 108 ++++++++++++++++++ 5 files changed, 272 insertions(+), 13 deletions(-) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index 1613692cd..26d89250b 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -9,6 +9,7 @@ def get_users_list( db: Session, admin: bool, edition_name: str | None, + exclude_edition: str | None, name: str | None, page: int ) -> UsersListResponse: @@ -22,10 +23,17 @@ def get_users_list( if admin: users_orm = users_crud.get_admins_page(db, page, name) else: - if edition_name is None: - users_orm = users_crud.get_users_page(db, page, name) + if exclude_edition is None: + if edition_name is None: + users_orm = users_crud.get_users_page(db, page, name) + else : + users_orm = users_crud.get_users_for_edition_page(db, edition_name, page, name) else: - users_orm = users_crud.get_users_for_edition_page(db, edition_name, page, name) + if edition_name is None: + users_orm = users_crud.get_users_exclude_edition_page(db, page, exclude_edition, name) + else: + users_orm = users_crud.get_users_for_edition_exclude_edition_page(db, page, exclude_edition, + edition_name, name) users = [] for user in users_orm: diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index 9ae446189..9923a0816 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -16,16 +16,17 @@ async def get_users( admin: bool = Query(False), edition: str | None = Query(None), + exclude_edition: str | None = Query(None), name: str | None = Query(None), page: int = 0, db: Session = Depends(get_session)): """ Get users - When the admin parameter is True, the edition parameter will have no effect. + 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, admin, edition, name, page) + return logic.get_users_list(db, admin, edition, exclude_edition, name, page) @users_router.get("/current", response_model=UserData) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index ad63dee92..4194ff539 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -22,7 +22,7 @@ def get_admins(db: Session) -> list[User]: return _get_admins_query(db).all() -def get_admins_page(db: Session, page: int, name: str = "") -> list[User]: +def get_admins_page(db: Session, page: int, name: str = "") -> list[User]: """ Get all admins paginated """ @@ -59,11 +59,11 @@ def get_user_edition_names(db: Session, user: User) -> list[str]: return editions -def _get_users_for_edition_query(db: Session, edition: Edition, name = "") -> Query: - return db\ - .query(User)\ - .where(User.name.contains(name))\ - .join(user_editions)\ +def _get_users_for_edition_query(db: Session, edition: Edition, name="") -> Query: + return db \ + .query(User) \ + .where(User.name.contains(name)) \ + .join(user_editions) \ .filter(user_editions.c.edition_id == edition.edition_id) @@ -74,13 +74,50 @@ def get_users_for_edition(db: Session, edition_name: str) -> list[User]: return _get_users_for_edition_query(db, get_edition_by_name(db, edition_name)).all() -def get_users_for_edition_page(db: Session, edition_name: str, page: int, name = "") -> list[User]: +def get_users_for_edition_page(db: Session, edition_name: str, page: int, name="") -> list[User]: """ Get all coaches from the given edition """ return paginate(_get_users_for_edition_query(db, get_edition_by_name(db, edition_name), name), page).all() +def get_users_for_edition_exclude_edition_page(db: Session, page: int, exclude_edition_name: str, edition_name: str, + name: str): + """ + Get all coaches from the given edition except those who are coach in the excluded edition + """ + + exclude_edition = get_edition_by_name(db, exclude_edition_name) + + return paginate( + _get_users_for_edition_query(db, get_edition_by_name(db, edition_name), name) + .filter(User.user_id.not_in( + db.query(user_editions.c.user_id).where(user_editions.c.edition_id == exclude_edition.edition_id) + ) + ) + , page).all() + + +def get_users_exclude_edition_page(db: Session, page: int, exclude_edition: str, name: str): + """ + Get all users who are not coach in the given edition + """ + + edition = get_edition_by_name(db, exclude_edition) + + return paginate( + db + .query(User) + .where(User.name.contains(name)) + .join(user_editions) + .filter( + User.user_id.not_in( + db.query(user_editions.c.user_id).where(user_editions.c.edition_id == edition.edition_id) + ) + ) + , page).all() + + def edit_admin_status(db: Session, user_id: int, admin: bool): """ Edit the admin-status of a user diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index f10c515c8..e5de6a9b4 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -232,13 +232,118 @@ def test_get_all_users_for_edition_paginated_filter_name(database_session: Sessi assert len(users_crud.get_users_for_edition_page(database_session, edition_1.name, 0, name="1")) == \ min(count, DB_PAGE_SIZE) assert len(users_crud.get_users_for_edition_page(database_session, edition_1.name, 1, name="1")) == \ - max(count - round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE, 0) + max(count - DB_PAGE_SIZE, 0) assert len(users_crud.get_users_for_edition_page(database_session, edition_2.name, 0, name="1")) == \ min(count, DB_PAGE_SIZE) assert len(users_crud.get_users_for_edition_page(database_session, edition_2.name, 1, name="1")) == \ max(count - DB_PAGE_SIZE, 0) +def test_get_all_users_excluded_edition_paginated(database_session: Session): + 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() + + 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(), [ + {"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 = users_crud.get_users_exclude_edition_page(database_session, 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_exclude_edition_page(database_session, 1, exclude_edition="edB", name="")) == \ + round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE + + b_users = users_crud.get_users_exclude_edition_page(database_session, 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_exclude_edition_page(database_session, 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): + 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() + + count = 0 + 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(), [ + {"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 = users_crud.get_users_exclude_edition_page(database_session, 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_exclude_edition_page(database_session, 1, exclude_edition="edB", name="1")) == \ + max(count - DB_PAGE_SIZE, 0) + + b_users = users_crud.get_users_exclude_edition_page(database_session, 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_exclude_edition_page(database_session, 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): + 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() + + correct_users = [] + 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(), [ + {"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(), [ + {"user_id": user_1.user_id, "edition_id": edition_b.edition_id}, + ]) + else: + correct_users.append(user_1) + + database_session.commit() + + users = users_crud.get_users_for_edition_exclude_edition_page(database_session, 0, exclude_edition_name="edB", + edition_name="edA", name="") + assert len(users) == len(correct_users) + for user in users: + assert user in correct_users + + def test_edit_admin_status(database_session: Session): """Test changing the admin status of a user""" diff --git a/backend/tests/test_routers/test_users/test_users.py b/backend/tests/test_routers/test_users/test_users.py index 59c799e7f..6f71ec334 100644 --- a/backend/tests/test_routers/test_users/test_users.py +++ b/backend/tests/test_routers/test_users/test_users.py @@ -207,6 +207,7 @@ def test_get_all_users_for_edition_paginated_filter_user(database_session: Sessi 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]): """Test endpoint for getting a list of admins, edition should be ignored""" auth_client.admin() @@ -220,6 +221,113 @@ def test_get_admins_from_edition(database_session: Session, auth_client: AuthCli assert len(response.json()['users']) == 2 +def test_get_all_users_excluded_edition_paginated(database_session: Session, auth_client: AuthClient): + 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() + + 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(), [ + {"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 + + 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 + + +def test_get_all_users_excluded_edition_paginated_filter_name(database_session: Session, auth_client: AuthClient): + 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() + + count = 0 + 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(), [ + {"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() + 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() + + correct_users_id = [] + 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(), [ + {"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(), [ + {"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 + + def test_get_users_invalid(database_session: Session, auth_client: AuthClient, data: dict[str, str | int]): """Test endpoint for unvalid input""" auth_client.admin() From d9cf651ce64699131453050281b387230e5cd4ae Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Mon, 11 Apr 2022 21:15:06 +0200 Subject: [PATCH 350/536] addCoach pagination --- .../UsersComponents/Coaches/Coaches.tsx | 5 +- .../Coaches/CoachesComponents/AddCoach.tsx | 112 +++++++++++++----- frontend/src/utils/api/users/coaches.ts | 2 + frontend/src/utils/api/users/users.ts | 17 ++- frontend/src/views/AdminsPage/AdminsPage.tsx | 2 +- frontend/src/views/UsersPage/UsersPage.tsx | 14 +-- 6 files changed, 102 insertions(+), 50 deletions(-) diff --git a/frontend/src/components/UsersComponents/Coaches/Coaches.tsx b/frontend/src/components/UsersComponents/Coaches/Coaches.tsx index 0b0951c67..dcb7d50ea 100644 --- a/frontend/src/components/UsersComponents/Coaches/Coaches.tsx +++ b/frontend/src/components/UsersComponents/Coaches/Coaches.tsx @@ -10,7 +10,6 @@ import { Spinner } from "react-bootstrap"; * This includes a searchfield and the option to remove and add coaches. * @param props.edition The edition of which coaches need to be shown. * @param props.coaches The list of all coaches of the current edition. - * @param props.users A list of all users who can be added as coach. * @param props.refresh A function which will be called when a coach is added/removed. * @param props.getMoreCoaches A function to load more coaches. * @param props.gotData All data is received. @@ -22,7 +21,6 @@ import { Spinner } from "react-bootstrap"; export default function Coaches(props: { edition: string; coaches: User[]; - users: User[]; refresh: () => void; getMoreCoaches: (page: number) => void; searchCoaches: (word: string) => void; @@ -31,6 +29,7 @@ export default function Coaches(props: { error: string; moreCoachesAvailable: boolean; searchTerm: string; + coachAdded: (user: User) => void; }) { let table; if (props.coaches.length === 0) { @@ -66,7 +65,7 @@ export default function Coaches(props: { value={props.searchTerm} onChange={e => props.searchCoaches(e.target.value)} /> - + {table} ); diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx index 4fa804997..c6b186401 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx @@ -1,41 +1,98 @@ -import { User } from "../../../../utils/api/users/users"; +import { getUsers, User } from "../../../../utils/api/users/users"; import React, { useState } from "react"; import { addCoachToEdition } from "../../../../utils/api/users/coaches"; -import { Button, Modal } from "react-bootstrap"; -import { Typeahead } from "react-bootstrap-typeahead"; +import { Button, Modal, Spinner } from "react-bootstrap"; +import { AsyncTypeahead } from "react-bootstrap-typeahead"; import { Error } from "../../PendingRequests/styles"; import { AddAdminButton, ModalContentConfirm } from "../../../AdminsComponents/styles"; /** * A button and popup to add a new coach to the given edition. * The popup consists of a field to search for a user. - * @param props.users A list of all users which can be added as coach to the edition. * @param props.edition The edition to which users need to be added. - * @param props.refresh A function which will be called when a user is added as coach. + * @param props.coachAdded A function which will be called when a user is added as coach. */ -export default function AddCoach(props: { users: User[]; edition: string; refresh: () => void }) { +export default function AddCoach(props: { edition: string; coachAdded: (user: User) => void }) { 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 + + async function getData(page: number, filter: string | undefined = undefined) { + if (filter === undefined) { + filter = searchTerm; + } + setGettingData(true); + setError(""); + try { + const response = await getUsers(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); + } + } + + function filterData(searchTerm: string) { + setSearchTerm(searchTerm); + setUsers([]); + getData(0, searchTerm); + } const handleClose = () => { setSelected(undefined); + setError(""); setShow(false); }; - const handleShow = () => setShow(true); + const handleShow = () => { + setShow(true); + }; - async function addCoach(userId: number) { + async function addCoach(user: User) { + setLoading(true); + setError(""); + let success = false; try { - const added = await addCoachToEdition(userId, props.edition); - if (added) { - props.refresh(); - handleClose(); - } else { + 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"); } + setLoading(false); + if (success) { + props.coachAdded(user); + handleClose(); + } + } + + let addButton; + if (loading) { + addButton = ; + } else { + addButton = ( + + ); } return ( @@ -50,30 +107,23 @@ export default function AddCoach(props: { users: User[]; edition: string; refres Add Coach - { - setSelected(selected[0] as User); - }} + { + setSelected(selected[0] as User); + setError(""); + }} /> - + {addButton} diff --git a/frontend/src/utils/api/users/coaches.ts b/frontend/src/utils/api/users/coaches.ts index 2e952db67..d675b5056 100644 --- a/frontend/src/utils/api/users/coaches.ts +++ b/frontend/src/utils/api/users/coaches.ts @@ -47,6 +47,8 @@ export async function removeCoachFromAllEditions(userId: number): Promise { + // eslint-disable-next-line promise/param-names + // await new Promise(r => setTimeout(r, 2000)); const response = await axiosInstance.post(`/users/${userId}/editions/${edition}`); return response.status === 204; } diff --git a/frontend/src/utils/api/users/users.ts b/frontend/src/utils/api/users/users.ts index 4c689c107..0cd3cc521 100644 --- a/frontend/src/utils/api/users/users.ts +++ b/frontend/src/utils/api/users/users.ts @@ -34,9 +34,20 @@ export async function getInviteLink(edition: string, email: string): Promise { - const response = await axiosInstance.get(`/users`); +export async function getUsers(edition: string, name: string, page: number): Promise { + // eslint-disable-next-line promise/param-names + // await new Promise(r => setTimeout(r, 2000)); + if (name) { + console.log(`/users/?page=${page}&exclude_edition=${edition}&name=${name}`); + const response = await axiosInstance.get( + `/users/?page=${page}&exclude_edition=${edition}&name=${name}` + ); + console.log(`|page: ${page} Search:${name} Found: ${response.data.users.length}`); + return response.data as UsersList; + } + const response = await axiosInstance.get(`/users/?exclude_edition=${edition}&page=${page}`); + console.log(`|page: ${page} Search:${name} Found: ${response.data.users.length}`); return response.data as UsersList; } diff --git a/frontend/src/views/AdminsPage/AdminsPage.tsx b/frontend/src/views/AdminsPage/AdminsPage.tsx index b764c22e8..357b89f90 100644 --- a/frontend/src/views/AdminsPage/AdminsPage.tsx +++ b/frontend/src/views/AdminsPage/AdminsPage.tsx @@ -22,7 +22,7 @@ export default function AdminsPage() { setAllAdmins(response.users); setAdmins(response.users); - const usersResponse = await getUsers(); + const usersResponse = await getUsers("", "", 0); const users = []; for (const user of usersResponse.users) { if (!response.users.some(e => e.userId === user.userId)) { diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index b648cf278..83500ca82 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -4,7 +4,7 @@ import { UsersPageDiv, AdminsButton, UsersHeader } from "./styles"; import { Coaches } from "../../components/UsersComponents/Coaches"; import { InviteUser } from "../../components/UsersComponents/InviteUser"; import { PendingRequests } from "../../components/UsersComponents/PendingRequests"; -import { getUsers, User } from "../../utils/api/users/users"; +import { User } from "../../utils/api/users/users"; import { getCoaches } from "../../utils/api/users/coaches"; /** @@ -12,7 +12,6 @@ import { getCoaches } from "../../utils/api/users/coaches"; */ function UsersPage() { const [coaches, setCoaches] = useState([]); // All coaches from the edition - const [users, setUsers] = useState([]); // All users which are not a coach const [gettingData, setGettingData] = useState(false); // Waiting for data const [gotData, setGotData] = useState(false); // Received data const [error, setError] = useState(""); // Error message @@ -39,15 +38,6 @@ function UsersPage() { setCoaches(coaches.concat(coachResponse.users)); } - const usersResponse = await getUsers(); - const users: User[] = []; - for (const user of usersResponse.users) { - if (!coachResponse.users.some(e => e.userId === user.userId)) { - users.push(user); - } - } - setUsers(users); - setGotData(true); setGettingData(false); } catch (exception) { @@ -93,7 +83,6 @@ function UsersPage() { getCoachesData(0)} gotData={gotData} gettingData={gettingData} @@ -102,6 +91,7 @@ function UsersPage() { searchCoaches={filterCoachesData} moreCoachesAvailable={moreCoachesAvailable} searchTerm={searchTerm} + coachAdded={coachAdded} /> ); From f03e33dc53d4395f023b686dc068907e19b50d9d Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Mon, 11 Apr 2022 21:22:43 +0200 Subject: [PATCH 351/536] Fix users with no edition not showing when excluding an edition --- backend/src/database/crud/users.py | 5 ++--- backend/tests/test_routers/test_users/test_users.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 4194ff539..687a26352 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -82,7 +82,7 @@ def get_users_for_edition_page(db: Session, edition_name: str, page: int, name=" def get_users_for_edition_exclude_edition_page(db: Session, page: int, exclude_edition_name: str, edition_name: str, - name: str): + name: str) -> list[User]: """ Get all coaches from the given edition except those who are coach in the excluded edition """ @@ -98,7 +98,7 @@ def get_users_for_edition_exclude_edition_page(db: Session, page: int, exclude_e , page).all() -def get_users_exclude_edition_page(db: Session, page: int, exclude_edition: str, name: str): +def get_users_exclude_edition_page(db: Session, page: int, exclude_edition: str, name: str) -> list[User]: """ Get all users who are not coach in the given edition """ @@ -109,7 +109,6 @@ def get_users_exclude_edition_page(db: Session, page: int, exclude_edition: str, db .query(User) .where(User.name.contains(name)) - .join(user_editions) .filter( User.user_id.not_in( db.query(user_editions.c.user_id).where(user_editions.c.edition_id == edition.edition_id) diff --git a/backend/tests/test_routers/test_users/test_users.py b/backend/tests/test_routers/test_users/test_users.py index 6f71ec334..33847ca23 100644 --- a/backend/tests/test_routers/test_users/test_users.py +++ b/backend/tests/test_routers/test_users/test_users.py @@ -246,14 +246,14 @@ def test_get_all_users_excluded_edition_paginated(database_session: Session, aut 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 + 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 + 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): From bcd9225369210d10ee83da8133eda36956f3ab9c Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Tue, 12 Apr 2022 10:39:03 +0200 Subject: [PATCH 352/536] addCoach async search --- .../components/AdminsComponents/AddAdmin.tsx | 105 +++++++++++++----- .../components/AdminsComponents/AdminList.tsx | 2 + .../Coaches/CoachesComponents/AddCoach.tsx | 4 +- frontend/src/utils/api/users/admins.ts | 10 +- frontend/src/utils/api/users/users.ts | 22 +++- frontend/src/views/AdminsPage/AdminsPage.tsx | 91 ++++++++++----- frontend/src/views/UsersPage/UsersPage.tsx | 1 - 7 files changed, 172 insertions(+), 63 deletions(-) diff --git a/frontend/src/components/AdminsComponents/AddAdmin.tsx b/frontend/src/components/AdminsComponents/AddAdmin.tsx index 38ef18c72..87080ae2d 100644 --- a/frontend/src/components/AdminsComponents/AddAdmin.tsx +++ b/frontend/src/components/AdminsComponents/AddAdmin.tsx @@ -1,9 +1,9 @@ -import { User } from "../../utils/api/users/users"; +import { getUsersNonAdmin, User } from "../../utils/api/users/users"; import React, { useState } from "react"; import { addAdmin } from "../../utils/api/users/admins"; import { AddAdminButton, ModalContentConfirm, Warning } from "./styles"; -import { Button, Modal } from "react-bootstrap"; -import { Typeahead } from "react-bootstrap-typeahead"; +import { Button, Modal, Spinner } from "react-bootstrap"; +import { AsyncTypeahead } from "react-bootstrap-typeahead"; import { Error } from "../UsersComponents/PendingRequests/styles"; /** @@ -26,32 +26,87 @@ function AddWarning(props: { name: string | undefined }) { * @param props.users All users which can be added as admin. * @param props.refresh A function which is called when a new admin is added. */ -export default function AddAdmin(props: { users: User[]; refresh: () => void }) { +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 + + 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); + } + } + + function filterData(searchTerm: string) { + setSearchTerm(searchTerm); + setUsers([]); + getData(0, searchTerm); + } const handleClose = () => { setSelected(undefined); + setError(""); setShow(false); }; const handleShow = () => { setShow(true); - setError(""); }; - async function addUserAsAdmin(userId: number) { + async function addUserAsAdmin(user: User) { + setLoading(true); + setError(""); + let success = false; try { - const added = await addAdmin(userId); - if (added) { - props.refresh(); - handleClose(); - } else { + 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(); + } + } + + let addButton; + if (loading) { + addButton = ; + } else { + addButton = ( + + ); } return ( @@ -66,30 +121,24 @@ export default function AddAdmin(props: { users: User[]; refresh: () => void }) Add Admin - { setSelected(selected[0] as User); + setError(""); }} - id="non-admin-users" - options={props.users} - labelKey="name" - emptyLabel="No users found." - placeholder={"name"} /> - + {addButton} diff --git a/frontend/src/components/AdminsComponents/AdminList.tsx b/frontend/src/components/AdminsComponents/AdminList.tsx index 85c13c267..8fb33960c 100644 --- a/frontend/src/components/AdminsComponents/AdminList.tsx +++ b/frontend/src/components/AdminsComponents/AdminList.tsx @@ -18,6 +18,8 @@ export default function AdminList(props: { loading: boolean; gotData: boolean; refresh: () => void; + getMoreAdmins: (page: number) => void; + moreAdminsAvailable: boolean; }) { if (props.loading) { return ( diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx index c6b186401..aafbf59fd 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx @@ -1,4 +1,4 @@ -import { getUsers, User } from "../../../../utils/api/users/users"; +import { getUsersExcludeEdition, User } from "../../../../utils/api/users/users"; import React, { useState } from "react"; import { addCoachToEdition } from "../../../../utils/api/users/coaches"; import { Button, Modal, Spinner } from "react-bootstrap"; @@ -28,7 +28,7 @@ export default function AddCoach(props: { edition: string; coachAdded: (user: Us setGettingData(true); setError(""); try { - const response = await getUsers(props.edition, filter, page); + const response = await getUsersExcludeEdition(props.edition, filter, page); if (page === 0) { setUsers(response.users); } else { diff --git a/frontend/src/utils/api/users/admins.ts b/frontend/src/utils/api/users/admins.ts index 695db192e..e0b520e69 100644 --- a/frontend/src/utils/api/users/admins.ts +++ b/frontend/src/utils/api/users/admins.ts @@ -4,8 +4,14 @@ import { axiosInstance } from "../api"; /** * Get all admins */ -export async function getAdmins(): Promise { - const response = await axiosInstance.get(`/users?admin=true`); +export async function getAdmins(page: number, name: string): Promise { + if (name) { + const response = await axiosInstance.get(`/users?page=${page}&admin=true&name=${name}`); + // console.log(`|page: ${page} Search:${name} Found: ${response.data.users.length}`); + return response.data as UsersList; + } + const response = await axiosInstance.get(`/users?page=${page}&admin=true`); + // console.log(`|page: ${page} Search:${name} Found: ${response.data.users.length}`); return response.data as UsersList; } diff --git a/frontend/src/utils/api/users/users.ts b/frontend/src/utils/api/users/users.ts index 0cd3cc521..219d8ed45 100644 --- a/frontend/src/utils/api/users/users.ts +++ b/frontend/src/utils/api/users/users.ts @@ -36,7 +36,11 @@ export async function getInviteLink(edition: string, email: string): Promise { +export async function getUsersExcludeEdition( + edition: string, + name: string, + page: number +): Promise { // eslint-disable-next-line promise/param-names // await new Promise(r => setTimeout(r, 2000)); if (name) { @@ -51,3 +55,19 @@ export async function getUsers(edition: string, name: string, page: number): Pro console.log(`|page: ${page} Search:${name} Found: ${response.data.users.length}`); return response.data as UsersList; } + +/** + * Get all users who are not admin + */ +export async function getUsersNonAdmin(name: string, page: number): Promise { + // eslint-disable-next-line promise/param-names + // await new Promise(r => setTimeout(r, 2000)); + if (name) { + const response = await axiosInstance.get(`/users/?page=${page}&admin=false&name=${name}`); + console.log(`|page: ${page} Found: ${response.data.users.length}`); + return response.data as UsersList; + } + const response = await axiosInstance.get(`/users/?admin=false&page=${page}`); + console.log(`|page: ${page} Found: ${response.data.users.length}`); + return response.data as UsersList; +} diff --git a/frontend/src/views/AdminsPage/AdminsPage.tsx b/frontend/src/views/AdminsPage/AdminsPage.tsx index 357b89f90..be69dbf79 100644 --- a/frontend/src/views/AdminsPage/AdminsPage.tsx +++ b/frontend/src/views/AdminsPage/AdminsPage.tsx @@ -1,35 +1,39 @@ import React, { useEffect, useState } from "react"; import { AdminsContainer } from "./styles"; -import { getUsers, User } from "../../utils/api/users/users"; import { getAdmins } from "../../utils/api/users/admins"; -import { Error, SearchInput } from "../../components/UsersComponents/PendingRequests/styles"; +import { + Error, + SearchInput, + SpinnerContainer, +} from "../../components/UsersComponents/PendingRequests/styles"; import { AddAdmin, AdminList } from "../../components/AdminsComponents"; +import { Spinner } from "react-bootstrap"; +import { User } from "../../utils/api/users/users"; export default function AdminsPage() { - const [allAdmins, setAllAdmins] = useState([]); const [admins, setAdmins] = useState([]); - const [users, setUsers] = useState([]); const [gettingData, setGettingData] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [gotData, setGotData] = useState(false); const [error, setError] = useState(""); + const [moreAdminsAvailable, setMoreAdminsAvailable] = useState(true); - async function getData() { + async function getData(page: number, filter: string | undefined = undefined) { + if (filter === undefined) { + filter = searchTerm; + } setGettingData(true); - setGotData(false); + setError(""); try { - const response = await getAdmins(); - setAllAdmins(response.users); - setAdmins(response.users); - - const usersResponse = await getUsers("", "", 0); - const users = []; - for (const user of usersResponse.users) { - if (!response.users.some(e => e.userId === user.userId)) { - users.push(user); - } + const response = await getAdmins(page, filter); + if (response.users.length !== 25) { + setMoreAdminsAvailable(false); + } + if (page === 0) { + setAdmins(response.users); + } else { + setAdmins(admins.concat(response.users)); } - setUsers(users); setGotData(true); setGettingData(false); @@ -41,26 +45,55 @@ export default function AdminsPage() { useEffect(() => { if (!gotData && !gettingData && !error) { - getData(); + getData(0); } - }, [gotData, gettingData, error, getData]); + }); - const filter = (word: string) => { + function filter(word: string) { + setGotData(false); setSearchTerm(word); - const newCoaches: User[] = []; - for (const admin of allAdmins) { - if (admin.name.toUpperCase().includes(word.toUpperCase())) { - newCoaches.push(admin); - } + setAdmins([]); + setMoreAdminsAvailable(true); + getData(0, word); + } + + function adminAdded(user: User) { + if (user.name.includes(searchTerm)) { + setAdmins([user].concat(admins)); + } + } + + let list; + if (admins.length === 0) { + if (gettingData) { + list = ( + + + + ); + } else if (gotData) { + list =
                                      No admins found
                                      ; + } else { + list = {error}; } - setAdmins(newCoaches); - }; + } else { + list = ( + getData(0)} + getMoreAdmins={getData} + moreAdminsAvailable={moreAdminsAvailable} + /> + ); + } return ( filter(e.target.value)} /> - - + + {list} {error} ); diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index 83500ca82..f2e5255f0 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -53,7 +53,6 @@ function UsersPage() { }); function filterCoachesData(searchTerm: string) { - setGettingData(true); setGotData(false); setSearchTerm(searchTerm); setCoaches([]); From 61040734de14c31545f9501f2b212792bff3cdbb Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Tue, 12 Apr 2022 11:58:47 +0200 Subject: [PATCH 353/536] Refactor GET /users --- backend/src/app/logic/users.py | 19 +-- backend/src/app/routers/users/users.py | 2 +- backend/src/database/crud/users.py | 118 ++++++------------ .../test_database/test_crud/test_users.py | 92 +++++++++----- .../test_routers/test_users/test_users.py | 50 ++++++++ 5 files changed, 149 insertions(+), 132 deletions(-) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index 26d89250b..c2ad85d2a 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -7,7 +7,7 @@ def get_users_list( db: Session, - admin: bool, + admin: bool | None, edition_name: str | None, exclude_edition: str | None, name: str | None, @@ -17,23 +17,8 @@ def get_users_list( Query the database for a list of users and wrap the result in a pydantic model """ - if name is None: - name = "" - if admin: - users_orm = users_crud.get_admins_page(db, page, name) - else: - if exclude_edition is None: - if edition_name is None: - users_orm = users_crud.get_users_page(db, page, name) - else : - users_orm = users_crud.get_users_for_edition_page(db, edition_name, page, name) - else: - if edition_name is None: - users_orm = users_crud.get_users_exclude_edition_page(db, page, exclude_edition, name) - else: - users_orm = users_crud.get_users_for_edition_exclude_edition_page(db, page, exclude_edition, - edition_name, name) + users_orm = users_crud.get_users_filtered(db, admin, edition_name, exclude_edition, name, page) users = [] for user in users_orm: diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index 9923a0816..10011ca41 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -14,7 +14,7 @@ @users_router.get("/", response_model=UsersListResponse, dependencies=[Depends(require_admin)]) async def get_users( - admin: bool = Query(False), + admin: bool = Query(None), edition: str | None = Query(None), exclude_edition: str | None = Query(None), name: str | None = Query(None), diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 687a26352..3a4a36847 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -2,47 +2,10 @@ from src.database.crud.editions import get_edition_by_name from src.database.crud.util import paginate -from src.database.models import user_editions, User, Edition, CoachRequest, AuthGoogle, AuthEmail, AuthGitHub +from src.database.models import user_editions, User, Edition, CoachRequest from src.database.crud.editions import get_editions -def _get_admins_query(db: Session, name: str = "") -> Query: - return db.query(User) \ - .where(User.admin) \ - .where(User.name.contains(name)) \ - .join(AuthEmail, isouter=True) \ - .join(AuthGitHub, isouter=True) \ - .join(AuthGoogle, isouter=True) - - -def get_admins(db: Session) -> list[User]: - """ - Get all admins - """ - return _get_admins_query(db).all() - - -def get_admins_page(db: Session, page: int, name: str = "") -> list[User]: - """ - Get all admins paginated - """ - return paginate(_get_admins_query(db, name), page).all() - - -def _get_users_query(db: Session, name: str = "") -> Query: - return db.query(User).where(User.name.contains(name)) - - -def get_users(db: Session) -> list[User]: - """Get all users (coaches + admins)""" - return _get_users_query(db).all() - - -def get_users_page(db: Session, page: int, name: str = "") -> list[User]: - """Get all users (coaches + admins) paginated""" - return paginate(_get_users_query(db, name), page).all() - - def get_user_edition_names(db: Session, user: User) -> list[str]: """Get all names of the editions this user can see""" # For admins: return all editions - otherwise, all editions this user is verified coach in @@ -59,62 +22,55 @@ def get_user_edition_names(db: Session, user: User) -> list[str]: return editions -def _get_users_for_edition_query(db: Session, edition: Edition, name="") -> Query: - return db \ - .query(User) \ - .where(User.name.contains(name)) \ - .join(user_editions) \ - .filter(user_editions.c.edition_id == edition.edition_id) - - -def get_users_for_edition(db: Session, edition_name: str) -> list[User]: - """ - Get all coaches from the given edition +def get_users_filtered( + db: Session, + admin: bool | None = None, + edition_name: str | None = None, + exclude_edition_name: str | None = None, + name: str | None = None, + page: int = 0 +): """ - return _get_users_for_edition_query(db, get_edition_by_name(db, edition_name)).all() + Get users and filter by optional parameter: + - admin: only return admins / only return non-admins + - edition_name: only return users who are coach of the given edition + - exclude_edition_name: only return users who are not coach of the given edition + - name: a string which the user's name must contain + - page: the page to return - -def get_users_for_edition_page(db: Session, edition_name: str, page: int, name="") -> list[User]: - """ - Get all coaches from the given edition + Note: When the admin parameter is set, edition_name and exclude_edition_name will be ignored. """ - return paginate(_get_users_for_edition_query(db, get_edition_by_name(db, edition_name), name), page).all() + query = db.query(User)\ -def get_users_for_edition_exclude_edition_page(db: Session, page: int, exclude_edition_name: str, edition_name: str, - name: str) -> list[User]: - """ - Get all coaches from the given edition except those who are coach in the excluded edition - """ + if name is not None: + query = query.where(User.name.contains(name)) - exclude_edition = get_edition_by_name(db, exclude_edition_name) + if admin is not None: + if admin: + query = query.filter(User.admin) + else: + query = query.filter(~User.admin) + # If admin parameter is set, edition & exclude_edition is ignored + return paginate(query, page).all() - return paginate( - _get_users_for_edition_query(db, get_edition_by_name(db, edition_name), name) - .filter(User.user_id.not_in( - db.query(user_editions.c.user_id).where(user_editions.c.edition_id == exclude_edition.edition_id) - ) - ) - , page).all() + if edition_name is not None: + edition = get_edition_by_name(db, edition_name) + query = query \ + .join(user_editions) \ + .filter(user_editions.c.edition_id == edition.edition_id) -def get_users_exclude_edition_page(db: Session, page: int, exclude_edition: str, name: str) -> list[User]: - """ - Get all users who are not coach in the given edition - """ + if exclude_edition_name is not None: + exclude_edition = get_edition_by_name(db, exclude_edition_name) - edition = get_edition_by_name(db, exclude_edition) - - return paginate( - db - .query(User) - .where(User.name.contains(name)) - .filter( + query = query.filter( User.user_id.not_in( - db.query(user_editions.c.user_id).where(user_editions.c.edition_id == edition.edition_id) + db.query(user_editions.c.user_id).where(user_editions.c.edition_id == exclude_edition.edition_id) ) ) - , page).all() + + return paginate(query, page).all() def edit_admin_status(db: Session, user_id: int, admin: bool): diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index e5de6a9b4..d6acca221 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -50,7 +50,7 @@ def test_get_all_users(database_session: Session, data: dict[str, int]): """Test get request for users""" # get all users - users = users_crud.get_users(database_session) + users = users_crud.get_users_filtered(database_session) assert len(users) == 2, "Wrong length" user_ids = [user.user_id for user in users] assert data["user1"] in user_ids @@ -62,8 +62,8 @@ def test_get_all_users_paginated(database_session: Session): database_session.add(models.User(name=f"User {i}", admin=False)) database_session.commit() - assert len(users_crud.get_users_page(database_session, 0)) == DB_PAGE_SIZE - assert len(users_crud.get_users_page(database_session, 1)) == round( + assert len(users_crud.get_users_filtered(database_session, page=0)) == DB_PAGE_SIZE + assert len(users_crud.get_users_filtered(database_session, page=1)) == round( DB_PAGE_SIZE * 1.5 ) - DB_PAGE_SIZE @@ -76,8 +76,8 @@ def test_get_all_users_paginated_filter_name(database_session: Session): count += 1 database_session.commit() - assert len(users_crud.get_users_page(database_session, 0, name="1")) == count - assert len(users_crud.get_users_page(database_session, 1, name="1")) == max(count - round( + assert len(users_crud.get_users_filtered(database_session, page=0, name="1")) == count + assert len(users_crud.get_users_filtered(database_session, page=1, name="1")) == max(count - round( DB_PAGE_SIZE * 1.5), 0) @@ -85,21 +85,47 @@ def test_get_all_admins(database_session: Session, data: dict[str, str]): """Test get request for admins""" # get all admins - users = users_crud.get_admins(database_session) + users = users_crud.get_users_filtered(database_session, admin=True) assert len(users) == 1, "Wrong length" assert data["user1"] == users[0].user_id def test_get_all_admins_paginated(database_session: Session): - count = 0 + admins = [] for i in range(round(DB_PAGE_SIZE * 3)): - database_session.add(models.User(name=f"User {i}", admin=i % 2 == 0)) + user = models.User(name=f"User {i}", admin=i % 2 == 0) + database_session.add(user) if i % 2 == 0: - count += 1 + admins.append(user) + database_session.commit() + + count = len(admins) + users = users_crud.get_users_filtered(database_session, 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(database_session, page=1, admin=True)) == \ + min(count - DB_PAGE_SIZE, DB_PAGE_SIZE) + + +def test_get_all_non_admins_paginated(database_session: Session): + 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() - assert len(users_crud.get_admins_page(database_session, 0)) == min(count, DB_PAGE_SIZE) - assert len(users_crud.get_admins_page(database_session, 1)) == min(count - DB_PAGE_SIZE, DB_PAGE_SIZE) + count = len(non_admins) + users = users_crud.get_users_filtered(database_session, 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(database_session, page=1, admin=False)) == \ + min(count - DB_PAGE_SIZE, DB_PAGE_SIZE) def test_get_all_admins_paginated_filter_name(database_session: Session): @@ -110,8 +136,8 @@ def test_get_all_admins_paginated_filter_name(database_session: Session): count += 1 database_session.commit() - assert len(users_crud.get_admins_page(database_session, 0, name="1")) == count - assert len(users_crud.get_admins_page(database_session, 1, name="1")) == max(count - round( + assert len(users_crud.get_users_filtered(database_session, page=0, name="1", admin=True)) == count + assert len(users_crud.get_users_filtered(database_session, page=1, name="1", admin=True)) == max(count - round( DB_PAGE_SIZE * 1.5), 0) @@ -167,13 +193,13 @@ def test_get_all_users_from_edition(database_session: Session, data: dict[str, s """Test get request for users of a given edition""" # get all users from edition - users = users_crud.get_users_for_edition(database_session, data["edition1"]) + users = users_crud.get_users_filtered(database_session, edition_name=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_for_edition(database_session, data["edition2"]) + users = users_crud.get_users_filtered(database_session, edition_name=data["edition2"]) assert len(users) == 1, "Wrong length" assert data["user2"] == users[0].user_id @@ -197,12 +223,12 @@ def test_get_all_users_for_edition_paginated(database_session: Session): ]) database_session.commit() - assert len(users_crud.get_users_for_edition_page(database_session, edition_1.name, 0)) == DB_PAGE_SIZE - assert len(users_crud.get_users_for_edition_page(database_session, edition_1.name, 1)) == round( + assert len(users_crud.get_users_filtered(database_session, edition_name=edition_1.name, page=0)) == DB_PAGE_SIZE + assert len(users_crud.get_users_filtered(database_session, edition_name=edition_1.name, page=1)) == round( DB_PAGE_SIZE * 1.5 ) - DB_PAGE_SIZE - assert len(users_crud.get_users_for_edition_page(database_session, edition_2.name, 0)) == DB_PAGE_SIZE - assert len(users_crud.get_users_for_edition_page(database_session, edition_2.name, 1)) == round( + assert len(users_crud.get_users_filtered(database_session, edition_name=edition_2.name, page=0)) == DB_PAGE_SIZE + assert len(users_crud.get_users_filtered(database_session, edition_name=edition_2.name, page=1)) == round( DB_PAGE_SIZE * 1.5 ) - DB_PAGE_SIZE @@ -229,13 +255,13 @@ def test_get_all_users_for_edition_paginated_filter_name(database_session: Sessi count += 1 database_session.commit() - assert len(users_crud.get_users_for_edition_page(database_session, edition_1.name, 0, name="1")) == \ + assert len(users_crud.get_users_filtered(database_session, edition_name=edition_1.name, page=0, name="1")) == \ min(count, DB_PAGE_SIZE) - assert len(users_crud.get_users_for_edition_page(database_session, edition_1.name, 1, name="1")) == \ + assert len(users_crud.get_users_filtered(database_session, edition_name=edition_1.name, page=1, name="1")) == \ max(count - DB_PAGE_SIZE, 0) - assert len(users_crud.get_users_for_edition_page(database_session, edition_2.name, 0, name="1")) == \ + assert len(users_crud.get_users_filtered(database_session, edition_name=edition_2.name, page=0, name="1")) == \ min(count, DB_PAGE_SIZE) - assert len(users_crud.get_users_for_edition_page(database_session, edition_2.name, 1, name="1")) == \ + assert len(users_crud.get_users_filtered(database_session, edition_name=edition_2.name, page=1, name="1")) == \ max(count - DB_PAGE_SIZE, 0) @@ -258,18 +284,18 @@ def test_get_all_users_excluded_edition_paginated(database_session: Session): ]) database_session.commit() - a_users = users_crud.get_users_exclude_edition_page(database_session, 0, exclude_edition="edB", name="") + a_users = users_crud.get_users_filtered(database_session, page=0, exclude_edition_name="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_exclude_edition_page(database_session, 1, exclude_edition="edB", name="")) == \ + assert len(users_crud.get_users_filtered(database_session, page=1, exclude_edition_name="edB", name="")) == \ round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE - b_users = users_crud.get_users_exclude_edition_page(database_session, 0, exclude_edition="edA", name="") + b_users = users_crud.get_users_filtered(database_session, page=0, exclude_edition_name="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_exclude_edition_page(database_session, 1, exclude_edition="edA", name="")) == \ + assert len(users_crud.get_users_filtered(database_session, page=1, exclude_edition_name="edA", name="")) == \ round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE @@ -295,18 +321,18 @@ def test_get_all_users_excluded_edition_paginated_filter_name(database_session: count += 1 database_session.commit() - a_users = users_crud.get_users_exclude_edition_page(database_session, 0, exclude_edition="edB", name="1") + a_users = users_crud.get_users_filtered(database_session, page=0, exclude_edition_name="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_exclude_edition_page(database_session, 1, exclude_edition="edB", name="1")) == \ + assert len(users_crud.get_users_filtered(database_session, page=1, exclude_edition_name="edB", name="1")) == \ max(count - DB_PAGE_SIZE, 0) - b_users = users_crud.get_users_exclude_edition_page(database_session, 0, exclude_edition="edA", name="1") + b_users = users_crud.get_users_filtered(database_session, page=0, exclude_edition_name="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_exclude_edition_page(database_session, 1, exclude_edition="edA", name="1")) == \ + assert len(users_crud.get_users_filtered(database_session, page=1, exclude_edition_name="edA", name="1")) == \ max(count - DB_PAGE_SIZE, 0) @@ -337,8 +363,8 @@ def test_get_all_users_for_edition_excluded_edition_paginated(database_session: database_session.commit() - users = users_crud.get_users_for_edition_exclude_edition_page(database_session, 0, exclude_edition_name="edB", - edition_name="edA", name="") + users = users_crud.get_users_filtered(database_session, page=0, exclude_edition_name="edB", + edition_name="edA") assert len(users) == len(correct_users) for user in users: assert user in correct_users diff --git a/backend/tests/test_routers/test_users/test_users.py b/backend/tests/test_routers/test_users/test_users.py index 33847ca23..278f157aa 100644 --- a/backend/tests/test_routers/test_users/test_users.py +++ b/backend/tests/test_routers/test_users/test_users.py @@ -143,6 +143,31 @@ def test_get_all_admins_paginated(database_session: Session, auth_client: AuthCl # +1 because Authclient.admin() also creates one user. +def test_get_all_non_admins_paginated(database_session: Session, 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() + if i % 2 != 0: + non_admins.append(user.user_id) + database_session.commit() + + 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): """Test endpoint for getting a list of paginated admins with filter for name""" for i in range(round(DB_PAGE_SIZE * 1.5)): @@ -158,6 +183,31 @@ def test_get_all_admins_paginated_filter_name(database_session: Session, auth_cl 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): + """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() + if i % 2 != 0 and "1" in str(i): + non_admins.append(user.user_id) + database_session.commit() + + 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 + + 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) + + def test_get_users_from_edition(database_session: Session, auth_client: AuthClient, data: dict[str, str | int]): """Test endpoint for getting a list of users from a given edition""" auth_client.admin() From e1882705b4d4416021047274adcef2635ced18b3 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 11 Apr 2022 16:10:56 +0200 Subject: [PATCH 354/536] Stash --- frontend/src/data/interfaces/editions.ts | 7 +++++++ frontend/src/data/interfaces/index.ts | 1 + .../src/views/EditionsPage/EditionsPage.tsx | 17 +++++++++++++++++ frontend/src/views/EditionsPage/index.ts | 1 + frontend/src/views/index.ts | 1 + 5 files changed, 27 insertions(+) create mode 100644 frontend/src/data/interfaces/editions.ts create mode 100644 frontend/src/views/EditionsPage/EditionsPage.tsx create mode 100644 frontend/src/views/EditionsPage/index.ts diff --git a/frontend/src/data/interfaces/editions.ts b/frontend/src/data/interfaces/editions.ts new file mode 100644 index 000000000..db1e88e65 --- /dev/null +++ b/frontend/src/data/interfaces/editions.ts @@ -0,0 +1,7 @@ +/** + * Data about an edition in the application + */ +export interface Edition { + name: string; + year: number; +} diff --git a/frontend/src/data/interfaces/index.ts b/frontend/src/data/interfaces/index.ts index 6d7c3694d..bb3f4dedb 100644 --- a/frontend/src/data/interfaces/index.ts +++ b/frontend/src/data/interfaces/index.ts @@ -1 +1,2 @@ +export type { Edition } from "./editions"; export type { User } from "./users"; diff --git a/frontend/src/views/EditionsPage/EditionsPage.tsx b/frontend/src/views/EditionsPage/EditionsPage.tsx new file mode 100644 index 000000000..170d57fea --- /dev/null +++ b/frontend/src/views/EditionsPage/EditionsPage.tsx @@ -0,0 +1,17 @@ +/** + * Page where users can see all editions they can access, + * and admins can delete editions. + */ +import { useEffect, useState } from "react"; +import { Edition } from "../../data/interfaces"; + +export default function EditionsPage() { + const [loading, setLoading] = useState(true); + const [editions, setEditions] = useState([]); + + return ( +
                                      +

                                      Editions!

                                      +
                                      + ); +} diff --git a/frontend/src/views/EditionsPage/index.ts b/frontend/src/views/EditionsPage/index.ts new file mode 100644 index 000000000..c00395635 --- /dev/null +++ b/frontend/src/views/EditionsPage/index.ts @@ -0,0 +1 @@ +export { default } from "./EditionsPage"; diff --git a/frontend/src/views/index.ts b/frontend/src/views/index.ts index 72c6efdc9..c378482b5 100644 --- a/frontend/src/views/index.ts +++ b/frontend/src/views/index.ts @@ -1,5 +1,6 @@ export * as Errors from "./errors"; export { default as LoginPage } from "./LoginPage"; +export { default as EditionsPage } from "./EditionsPage"; export { default as PendingPage } from "./PendingPage"; export { default as ProjectsPage } from "./ProjectsPage"; export { default as RegisterPage } from "./RegisterPage"; From 7a51630e15097218055180f61010c5657c70443c Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 11 Apr 2022 17:44:58 +0200 Subject: [PATCH 355/536] Create editions page --- frontend/src/Router.tsx | 6 ++-- .../EditionsPage/DeleteEditionButton.tsx | 26 ++++++++++++++ .../components/EditionsPage/EditionRow.tsx | 24 +++++++++++++ .../components/EditionsPage/EditionsTable.tsx | 34 +++++++++++++++++++ frontend/src/components/EditionsPage/index.ts | 1 + .../src/components/EditionsPage/styles.ts | 27 +++++++++++++++ frontend/src/contexts/auth-context.tsx | 2 +- frontend/src/utils/api/editions.ts | 14 ++++++++ .../src/views/EditionsPage/EditionsPage.tsx | 16 ++++----- frontend/src/views/EditionsPage/styles.ts | 11 ++++++ 10 files changed, 148 insertions(+), 13 deletions(-) create mode 100644 frontend/src/components/EditionsPage/DeleteEditionButton.tsx create mode 100644 frontend/src/components/EditionsPage/EditionRow.tsx create mode 100644 frontend/src/components/EditionsPage/EditionsTable.tsx create mode 100644 frontend/src/components/EditionsPage/index.ts create mode 100644 frontend/src/components/EditionsPage/styles.ts create mode 100644 frontend/src/utils/api/editions.ts create mode 100644 frontend/src/views/EditionsPage/styles.ts diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 5a3aac702..05a304c64 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -2,8 +2,9 @@ import React from "react"; import { Container, ContentWrapper } from "./app.styles"; import { BrowserRouter, Navigate, Outlet, Route, Routes } from "react-router-dom"; import { AdminRoute, Footer, Navbar, PrivateRoute } from "./components"; -import { useAuth } from "./contexts/auth-context"; +import { useAuth } from "./contexts"; import { + EditionsPage, LoginPage, PendingPage, ProjectsPage, @@ -43,8 +44,7 @@ export default function Router() { } /> }> - {/* TODO editions page */} - } /> + } /> }> {/* TODO create edition page */} } /> diff --git a/frontend/src/components/EditionsPage/DeleteEditionButton.tsx b/frontend/src/components/EditionsPage/DeleteEditionButton.tsx new file mode 100644 index 000000000..fb530fb42 --- /dev/null +++ b/frontend/src/components/EditionsPage/DeleteEditionButton.tsx @@ -0,0 +1,26 @@ +import { Edition } from "../../data/interfaces"; +import { DeleteButton } from "./styles"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons/faTriangleExclamation"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { useAuth } from "../../contexts"; +import { Role } from "../../data/enums"; + +interface Props { + edition: Edition; +} + +export default function DeleteEditionButton(props: Props) { + const { role } = useAuth(); + + // Only admins can see this button + if (role !== Role.ADMIN) { + return null; + } + + return ( + + Delete this edition + + ); +} diff --git a/frontend/src/components/EditionsPage/EditionRow.tsx b/frontend/src/components/EditionsPage/EditionRow.tsx new file mode 100644 index 000000000..aba87e499 --- /dev/null +++ b/frontend/src/components/EditionsPage/EditionRow.tsx @@ -0,0 +1,24 @@ +import { Edition } from "../../data/interfaces"; +import DeleteEditionButton from "./DeleteEditionButton"; +import { RowContainer } from "./styles"; + +interface Props { + edition: Edition; +} + +/** + * A row in the [[EditionsTable]] + */ +export default function EditionRow(props: Props) { + return ( + + +
                                      +

                                      {props.edition.name}

                                      + {props.edition.year} +
                                      + +
                                      + + ); +} diff --git a/frontend/src/components/EditionsPage/EditionsTable.tsx b/frontend/src/components/EditionsPage/EditionsTable.tsx new file mode 100644 index 000000000..85e46a0d7 --- /dev/null +++ b/frontend/src/components/EditionsPage/EditionsTable.tsx @@ -0,0 +1,34 @@ +import React, { useEffect, useState } from "react"; +import { StyledTable, LoadingSpinner } from "./styles"; +import { getEditions } from "../../utils/api/editions"; +import EditionRow from "./EditionRow"; + +export default function EditionsTable() { + const [loading, setLoading] = useState(true); + const [rows, setRows] = useState([]); + + async function loadEditions() { + const response = await getEditions(); + + const newRows: React.ReactNode[] = response.editions.map(edition => ( + + )); + setRows(newRows); + setLoading(false); + } + + useEffect(() => { + loadEditions(); + }, []); + + // Still loading: display a spinner instead + if (loading) { + return ; + } + + return ( + + {rows} + + ); +} diff --git a/frontend/src/components/EditionsPage/index.ts b/frontend/src/components/EditionsPage/index.ts new file mode 100644 index 000000000..978365fad --- /dev/null +++ b/frontend/src/components/EditionsPage/index.ts @@ -0,0 +1 @@ +export { default as EditionsTable } from "./EditionsTable"; diff --git a/frontend/src/components/EditionsPage/styles.ts b/frontend/src/components/EditionsPage/styles.ts new file mode 100644 index 000000000..eaefcceca --- /dev/null +++ b/frontend/src/components/EditionsPage/styles.ts @@ -0,0 +1,27 @@ +import styled from "styled-components"; +import Spinner from "react-bootstrap/Spinner"; +import Table from "react-bootstrap/Table"; +import Button from "react-bootstrap/Button"; + +export const StyledTable = styled(Table).attrs(() => ({ + striped: true, + bordered: true, + hover: true, + variant: "dark", + className: "m-0", +}))``; + +export const LoadingSpinner = styled(Spinner).attrs(() => ({ + animation: "border", + role: "status", + className: "mx-auto", +}))``; + +export const DeleteButton = styled(Button).attrs(() => ({ + variant: "danger", + className: "me-0 ms-auto my-auto", +}))``; + +export const RowContainer = styled.td.attrs(() => ({ + className: "p-3 d-flex", +}))``; diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx index 5b0f8a8cb..cf1794190 100644 --- a/frontend/src/contexts/auth-context.tsx +++ b/frontend/src/contexts/auth-context.tsx @@ -99,11 +99,11 @@ export function AuthProvider({ children }: { children: ReactNode }) { * Set the user's login data in the AuthContext */ export function logIn(user: User, token: string | null, authContext: AuthContextState) { - authContext.setIsLoggedIn(true); authContext.setUserId(user.userId); authContext.setRole(user.admin ? Role.ADMIN : Role.COACH); authContext.setEditions(user.editions); authContext.setToken(token); + authContext.setIsLoggedIn(true); } /** diff --git a/frontend/src/utils/api/editions.ts b/frontend/src/utils/api/editions.ts new file mode 100644 index 000000000..74a8ace37 --- /dev/null +++ b/frontend/src/utils/api/editions.ts @@ -0,0 +1,14 @@ +import { axiosInstance } from "./api"; +import { Edition } from "../../data/interfaces"; + +interface EditionsResponse { + editions: Edition[]; +} + +/** + * Get all editions the user can see. + */ +export async function getEditions(): Promise { + const response = await axiosInstance.get("/editions/"); + return response.data as EditionsResponse; +} diff --git a/frontend/src/views/EditionsPage/EditionsPage.tsx b/frontend/src/views/EditionsPage/EditionsPage.tsx index 170d57fea..1a17799eb 100644 --- a/frontend/src/views/EditionsPage/EditionsPage.tsx +++ b/frontend/src/views/EditionsPage/EditionsPage.tsx @@ -1,17 +1,15 @@ +import { EditionsTable } from "../../components/EditionsPage"; +import { EditionsPageContainer } from "./styles"; + /** * Page where users can see all editions they can access, * and admins can delete editions. */ -import { useEffect, useState } from "react"; -import { Edition } from "../../data/interfaces"; - export default function EditionsPage() { - const [loading, setLoading] = useState(true); - const [editions, setEditions] = useState([]); - return ( -
                                      -

                                      Editions!

                                      -
                                      + +

                                      Editions

                                      + +
                                      ); } diff --git a/frontend/src/views/EditionsPage/styles.ts b/frontend/src/views/EditionsPage/styles.ts new file mode 100644 index 000000000..cfafda700 --- /dev/null +++ b/frontend/src/views/EditionsPage/styles.ts @@ -0,0 +1,11 @@ +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; +`; From 91dae56899e0d6df7018a41d5d011fa17cbcffcf Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 11 Apr 2022 18:45:18 +0200 Subject: [PATCH 356/536] Add deletion modal --- .../EditionsPage/DeleteEditionButton.tsx | 10 +++- .../EditionsPage/DeleteEditionModal.css | 19 ++++++++ .../EditionsPage/DeleteEditionModal.tsx | 48 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/EditionsPage/DeleteEditionModal.css create mode 100644 frontend/src/components/EditionsPage/DeleteEditionModal.tsx diff --git a/frontend/src/components/EditionsPage/DeleteEditionButton.tsx b/frontend/src/components/EditionsPage/DeleteEditionButton.tsx index fb530fb42..ce138d544 100644 --- a/frontend/src/components/EditionsPage/DeleteEditionButton.tsx +++ b/frontend/src/components/EditionsPage/DeleteEditionButton.tsx @@ -5,6 +5,8 @@ import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons/faTrian import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { useAuth } from "../../contexts"; import { Role } from "../../data/enums"; +import React, { useState } from "react"; +import DeleteEditionModal from "./DeleteEditionModal"; interface Props { edition: Edition; @@ -12,15 +14,21 @@ interface Props { export default function DeleteEditionButton(props: Props) { const { role } = useAuth(); + const [showModal, setShowModal] = useState(false); // Only admins can see this button if (role !== Role.ADMIN) { return null; } + function handleClick() { + setShowModal(true); + } + return ( - + Delete this edition + ); } diff --git a/frontend/src/components/EditionsPage/DeleteEditionModal.css b/frontend/src/components/EditionsPage/DeleteEditionModal.css new file mode 100644 index 000000000..be982dfdd --- /dev/null +++ b/frontend/src/components/EditionsPage/DeleteEditionModal.css @@ -0,0 +1,19 @@ +.modal-dialog { + background-color: var(--osoc_blue); +} + +.modal-header { + background-color: var(--osoc_blue); +} + +.modal-body { + background-color: var(--background_color); +} + +.modal-footer { + background-color: var(--osoc_blue); +} + +.modal-content { + background-color: var(--osoc_blue); +} diff --git a/frontend/src/components/EditionsPage/DeleteEditionModal.tsx b/frontend/src/components/EditionsPage/DeleteEditionModal.tsx new file mode 100644 index 000000000..e0c5b7c56 --- /dev/null +++ b/frontend/src/components/EditionsPage/DeleteEditionModal.tsx @@ -0,0 +1,48 @@ +import { Edition } from "../../data/interfaces"; +import Modal from "react-bootstrap/Modal"; +import React from "react"; +import Button from "react-bootstrap/Button"; +import "./DeleteEditionModal.css"; + +interface Props { + edition: Edition; + show: boolean; + setShow: (value: boolean) => void; +} + +/** + * Modal shown when trying to delete an edition + */ +export default function DeleteEditionModal(props: Props) { + const disableConfirm = true; + + function handleClose() { + props.setShow(false); + } + + function handleConfirm() { + props.setShow(false); + } + + return ( + // Obscure JS thing: clicking "close" on the modal propagates the "onClick" up + // to the open button, which re-opens it + // Explicitly deny propagation to stop this +
                                      e.stopPropagation()}> + + + Easy, partner! + + You can't just walk around deleting editions like that! + + + + + +
                                      + ); +} From a9d527c1a9b87d969c47829680313f5e709ca5b1 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 11 Apr 2022 19:19:48 +0200 Subject: [PATCH 357/536] More work on modal --- .../EditionsPage/DeleteEditionButton.tsx | 2 +- .../EditionsPage/DeleteEditionModal.tsx | 48 ---------- .../DeleteEditionModal.css | 4 + .../DeleteEditionModal/DeleteEditionModal.tsx | 96 +++++++++++++++++++ .../DeleteEditionModal/InfoMessage.tsx | 30 ++++++ 5 files changed, 131 insertions(+), 49 deletions(-) delete mode 100644 frontend/src/components/EditionsPage/DeleteEditionModal.tsx rename frontend/src/components/EditionsPage/{ => DeleteEditionModal}/DeleteEditionModal.css (90%) create mode 100644 frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx create mode 100644 frontend/src/components/EditionsPage/DeleteEditionModal/InfoMessage.tsx diff --git a/frontend/src/components/EditionsPage/DeleteEditionButton.tsx b/frontend/src/components/EditionsPage/DeleteEditionButton.tsx index ce138d544..298464b7e 100644 --- a/frontend/src/components/EditionsPage/DeleteEditionButton.tsx +++ b/frontend/src/components/EditionsPage/DeleteEditionButton.tsx @@ -6,7 +6,7 @@ import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { useAuth } from "../../contexts"; import { Role } from "../../data/enums"; import React, { useState } from "react"; -import DeleteEditionModal from "./DeleteEditionModal"; +import DeleteEditionModal from "./DeleteEditionModal/DeleteEditionModal"; interface Props { edition: Edition; diff --git a/frontend/src/components/EditionsPage/DeleteEditionModal.tsx b/frontend/src/components/EditionsPage/DeleteEditionModal.tsx deleted file mode 100644 index e0c5b7c56..000000000 --- a/frontend/src/components/EditionsPage/DeleteEditionModal.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Edition } from "../../data/interfaces"; -import Modal from "react-bootstrap/Modal"; -import React from "react"; -import Button from "react-bootstrap/Button"; -import "./DeleteEditionModal.css"; - -interface Props { - edition: Edition; - show: boolean; - setShow: (value: boolean) => void; -} - -/** - * Modal shown when trying to delete an edition - */ -export default function DeleteEditionModal(props: Props) { - const disableConfirm = true; - - function handleClose() { - props.setShow(false); - } - - function handleConfirm() { - props.setShow(false); - } - - return ( - // Obscure JS thing: clicking "close" on the modal propagates the "onClick" up - // to the open button, which re-opens it - // Explicitly deny propagation to stop this -
                                      e.stopPropagation()}> - - - Easy, partner! - - You can't just walk around deleting editions like that! - - - - - -
                                      - ); -} diff --git a/frontend/src/components/EditionsPage/DeleteEditionModal.css b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.css similarity index 90% rename from frontend/src/components/EditionsPage/DeleteEditionModal.css rename to frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.css index be982dfdd..59af3fd5d 100644 --- a/frontend/src/components/EditionsPage/DeleteEditionModal.css +++ b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.css @@ -17,3 +17,7 @@ .modal-content { background-color: var(--osoc_blue); } + +.form-text { + color: white; +} \ No newline at end of file diff --git a/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx new file mode 100644 index 000000000..effab021e --- /dev/null +++ b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx @@ -0,0 +1,96 @@ +import { Edition } from "../../../data/interfaces"; +import Modal from "react-bootstrap/Modal"; +import React, { useState } from "react"; +import Button from "react-bootstrap/Button"; +import "./DeleteEditionModal.css"; +import Form from "react-bootstrap/Form"; +import InfoMessage from "./InfoMessage"; + +interface Props { + edition: Edition; + show: boolean; + setShow: (value: boolean) => void; +} + +/** + * Create a value for the slider to be on + */ +function createSliderValue(): number { + return Math.floor(Math.random() * 100); +} + +/** + * Modal shown when trying to delete an edition + */ +export default function DeleteEditionModal(props: Props) { + const [requiredSliderValue, setRequiredSliderValue] = useState(createSliderValue()); + const [understandClicked, setUnderstandClicked] = useState(false); + const [sliderValue, setSliderValue] = useState(0); + const disableConfirm = true; + + function handleClose() { + props.setShow(false); + setUnderstandClicked(false); + + // Create a new slider value + setRequiredSliderValue(createSliderValue()); + } + + function handleConfirm() { + props.setShow(false); + } + + return ( + // Obscure JS thing: clicking "close" on the modal propagates the "onClick" up + // to the open button, which re-opens it + // Explicitly deny propagation to stop this +
                                      e.stopPropagation()}> + + + Easy, partner! + + {/* Show checkbox screen if not clicked, else move on */} + {!understandClicked ? ( + +
                                      + + + setUnderstandClicked(e.target.checked)} + /> + + +
                                      + ) : ( + +
                                      + + You didn't think it would be that easy, did you? + + + Set the value of the slider below to {requiredSliderValue}% + + + setSliderValue(Number(e.target.value))} + /> + {sliderValue}% + +
                                      +
                                      + )} + + + + +
                                      +
                                      + ); +} diff --git a/frontend/src/components/EditionsPage/DeleteEditionModal/InfoMessage.tsx b/frontend/src/components/EditionsPage/DeleteEditionModal/InfoMessage.tsx new file mode 100644 index 000000000..2435b7ec0 --- /dev/null +++ b/frontend/src/components/EditionsPage/DeleteEditionModal/InfoMessage.tsx @@ -0,0 +1,30 @@ +import Form from "react-bootstrap/Form"; +import React from "react"; + +interface Props { + editionName: string; +} + +export default function InfoMessage(props: Props) { + return ( + <> + + Deleting {props.editionName} has some serious consequences that{" "} + + can never be undone + + . + +
                                      +
                                      + + This includes, but is not limited to, removal of: +
                                        +
                                      • the edition itself
                                      • +
                                      • all students linked to this edition
                                      • +
                                      • all projects linked to this edition
                                      • +
                                      +
                                      + + ); +} From be7252586a23a8e677e622d14fd14a397f83a992 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 11 Apr 2022 20:13:16 +0200 Subject: [PATCH 358/536] Remove slider for now --- .../DeleteEditionModal/DeleteEditionModal.css | 2 + .../DeleteEditionModal/DeleteEditionModal.tsx | 101 ++++++++++++------ 2 files changed, 71 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.css b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.css index 59af3fd5d..57168ea9b 100644 --- a/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.css +++ b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.css @@ -8,6 +8,8 @@ .modal-body { background-color: var(--background_color); + display: flex; + flex-direction: column; } .modal-footer { diff --git a/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx index effab021e..1640ddf62 100644 --- a/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx +++ b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx @@ -5,6 +5,7 @@ import Button from "react-bootstrap/Button"; import "./DeleteEditionModal.css"; import Form from "react-bootstrap/Form"; import InfoMessage from "./InfoMessage"; +import Spinner from "react-bootstrap/Spinner"; interface Props { edition: Edition; @@ -12,32 +13,44 @@ interface Props { setShow: (value: boolean) => void; } -/** - * Create a value for the slider to be on - */ -function createSliderValue(): number { - return Math.floor(Math.random() * 100); -} - /** * Modal shown when trying to delete an edition */ export default function DeleteEditionModal(props: Props) { - const [requiredSliderValue, setRequiredSliderValue] = useState(createSliderValue()); + const [disableConfirm, setDisableConfirm] = useState(true); const [understandClicked, setUnderstandClicked] = useState(false); - const [sliderValue, setSliderValue] = useState(0); - const disableConfirm = true; + const [confirmed, setConfirmed] = useState(false); function handleClose() { props.setShow(false); setUnderstandClicked(false); - - // Create a new slider value - setRequiredSliderValue(createSliderValue()); } function handleConfirm() { - props.setShow(false); + setConfirmed(true); + + // props.setShow(false); + // // Force-reload the page to re-request all data related to editions + // // (stored in various places such as auth, ...) + // window.location.reload(); + } + + /** + * Validate the data entered into the form + */ + function checkFormValid(name: string) { + if (name !== props.edition.name) { + setDisableConfirm(true); + } else { + setDisableConfirm(false); + } + } + + /** + * Function called when the input field for the name of the edition changes + */ + function handleTextfieldChange(value: string) { + checkFormValid(value); } return ( @@ -52,7 +65,7 @@ export default function DeleteEditionModal(props: Props) { {/* Show checkbox screen if not clicked, else move on */} {!understandClicked ? ( -
                                      + {}}>
                                      - ) : ( + ) : !confirmed ? ( + // Checkbox screen was passed, show the other screen now -
                                      - - You didn't think it would be that easy, did you? - + {}}> - Set the value of the slider below to {requiredSliderValue}% + You didn't think it would be that easy, did you? +
                                      +
                                      - setSliderValue(Number(e.target.value))} + + Type the name of the edition in the field below + + handleTextfieldChange(e.target.value)} /> - {sliderValue}%
                                      + ) : ( + // Delete request is being sent + +

                                      Deleting {props.edition.name}...

                                      +

                                      + The request has been sent, there's no turning back now! +

                                      + If you've changed your mind, all you can do now is hope the request + fails. +

                                      + +
                                      + )} + {/* Only show footer if not yet confirmed */} + {!confirmed && ( + + + + )} - - - -
                                      ); From 10b4cc18d0808551f90ec3ea9c5b153d62561ff7 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 11 Apr 2022 20:25:21 +0200 Subject: [PATCH 359/536] Send delete request --- .../DeleteEditionModal/DeleteEditionModal.tsx | 51 +++++++++++++------ frontend/src/utils/api/editions.ts | 8 +++ 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx index 1640ddf62..04cd13b08 100644 --- a/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx +++ b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx @@ -6,6 +6,11 @@ import "./DeleteEditionModal.css"; import Form from "react-bootstrap/Form"; import InfoMessage from "./InfoMessage"; import Spinner from "react-bootstrap/Spinner"; +import { deleteEdition } from "../../../utils/api/editions"; +import { + getCurrentEdition, + setCurrentEdition, +} from "../../../utils/session-storage/current-edition"; interface Props { edition: Edition; @@ -26,13 +31,29 @@ export default function DeleteEditionModal(props: Props) { setUnderstandClicked(false); } - function handleConfirm() { + /** + * Confirm the deletion of this edition + */ + async function handleConfirm() { + // Show confirmation text while the request is being sent setConfirmed(true); - // props.setShow(false); - // // Force-reload the page to re-request all data related to editions - // // (stored in various places such as auth, ...) - // window.location.reload(); + // Delete the request + const statusCode = await deleteEdition(props.edition.name); + + // Hide the modal + props.setShow(false); + + if (statusCode === 204) { + // Remove the edition as current + if (getCurrentEdition() === props.edition.name) { + setCurrentEdition(null); + } + + // Force-reload the page to re-request all data related to editions + // (stored in various places such as auth, ...) + window.location.reload(); + } } /** @@ -47,7 +68,7 @@ export default function DeleteEditionModal(props: Props) { } /** - * Function called when the input field for the name of the edition changes + * Called when the input field for the name of the edition changes */ function handleTextfieldChange(value: string) { checkFormValid(value); @@ -60,12 +81,14 @@ export default function DeleteEditionModal(props: Props) {
                                      e.stopPropagation()}> - Easy, partner! + + {confirmed ? `Deleting ${props.edition.name}...` : "Not so fast!"} + - {/* Show checkbox screen if not clicked, else move on */} {!understandClicked ? ( + // Show checkbox screen/information text -
                                      {}}> + - {}}> + You didn't think it would be that easy, did you? @@ -102,12 +125,10 @@ export default function DeleteEditionModal(props: Props) { ) : ( // Delete request is being sent -

                                      Deleting {props.edition.name}...

                                      +

                                      There's no turning back now!

                                      - The request has been sent, there's no turning back now! -

                                      - If you've changed your mind, all you can do now is hope the request - fails. + The request has been sent. If you've changed your mind, all you can do + now is hope the request fails.

                                      diff --git a/frontend/src/utils/api/editions.ts b/frontend/src/utils/api/editions.ts index 74a8ace37..ad05a3515 100644 --- a/frontend/src/utils/api/editions.ts +++ b/frontend/src/utils/api/editions.ts @@ -12,3 +12,11 @@ export async function getEditions(): Promise { const response = await axiosInstance.get("/editions/"); return response.data as EditionsResponse; } + +/** + * Delete an edition by name + */ +export async function deleteEdition(name: string): Promise { + const response = await axiosInstance.delete(`/editions/${name}`); + return response.status; +} From 70d3083daf5358c1e7c2a4c86605db8220bea068 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Tue, 12 Apr 2022 01:42:19 +0200 Subject: [PATCH 360/536] Button to create new editions --- frontend/src/App.css | 1 + frontend/src/Router.tsx | 3 ++- .../components/EditionsPage/EditionsTable.tsx | 17 +++++++++++++++++ .../EditionsPage/NewEditionButton.tsx | 12 ++++++++++++ frontend/src/components/EditionsPage/index.ts | 1 + frontend/src/components/EditionsPage/styles.ts | 6 +++++- .../CreateEditionPage/CreateEditionPage.tsx | 6 ++++++ frontend/src/views/CreateEditionPage/index.ts | 1 + .../src/views/EditionsPage/EditionsPage.tsx | 6 +++++- 9 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/EditionsPage/NewEditionButton.tsx create mode 100644 frontend/src/views/CreateEditionPage/CreateEditionPage.tsx create mode 100644 frontend/src/views/CreateEditionPage/index.ts diff --git a/frontend/src/App.css b/frontend/src/App.css index bee7b3085..3f0d1d775 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -10,6 +10,7 @@ #root { height: 100%; + overflow-x: hidden; } * { diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 05a304c64..8f4812a79 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -14,6 +14,7 @@ import { VerifyingTokenPage, } from "./views"; import { ForbiddenPage, NotFoundPage } from "./views/errors"; +import CreateEditionPage from "./views/CreateEditionPage"; /** * Router component to render different pages depending on the current url. Renders @@ -47,7 +48,7 @@ export default function Router() { } /> }> {/* TODO create edition page */} - } /> + } /> }> {/* TODO edition page? do we need? maybe just some nav/links? */} diff --git a/frontend/src/components/EditionsPage/EditionsTable.tsx b/frontend/src/components/EditionsPage/EditionsTable.tsx index 85e46a0d7..885cbb70f 100644 --- a/frontend/src/components/EditionsPage/EditionsTable.tsx +++ b/frontend/src/components/EditionsPage/EditionsTable.tsx @@ -3,6 +3,12 @@ import { StyledTable, LoadingSpinner } from "./styles"; import { getEditions } from "../../utils/api/editions"; import EditionRow from "./EditionRow"; +/** + * Table on the [[EditionsPage]] that renders a list of all editions + * that the user has access to. + * + * If the user is an admin, this will also render a delete button. + */ export default function EditionsTable() { const [loading, setLoading] = useState(true); const [rows, setRows] = useState([]); @@ -13,6 +19,7 @@ export default function EditionsTable() { const newRows: React.ReactNode[] = response.editions.map(edition => ( )); + setRows(newRows); setLoading(false); } @@ -26,6 +33,16 @@ export default function EditionsTable() { return ; } + if (rows.length === 0) { + return ( +
                                      + It looks like you're not a part of any editions so far. +
                                      + Contact an admin to receive an invite. +
                                      + ); + } + return ( {rows} diff --git a/frontend/src/components/EditionsPage/NewEditionButton.tsx b/frontend/src/components/EditionsPage/NewEditionButton.tsx new file mode 100644 index 000000000..0c2b1ed05 --- /dev/null +++ b/frontend/src/components/EditionsPage/NewEditionButton.tsx @@ -0,0 +1,12 @@ +import { StyledNewEditionButton } from "./styles"; + +interface Props { + onClick: () => void; +} + +/** + * Button to create a new edition, redirects to the [[CreateEditionPage]]. + */ +export default function NewEditionButton({ onClick }: Props) { + return Create new edition; +} diff --git a/frontend/src/components/EditionsPage/index.ts b/frontend/src/components/EditionsPage/index.ts index 978365fad..fd2d2e6ae 100644 --- a/frontend/src/components/EditionsPage/index.ts +++ b/frontend/src/components/EditionsPage/index.ts @@ -1 +1,2 @@ export { default as EditionsTable } from "./EditionsTable"; +export { default as NewEditionButton } from "./NewEditionButton"; diff --git a/frontend/src/components/EditionsPage/styles.ts b/frontend/src/components/EditionsPage/styles.ts index eaefcceca..c91fb2544 100644 --- a/frontend/src/components/EditionsPage/styles.ts +++ b/frontend/src/components/EditionsPage/styles.ts @@ -8,7 +8,7 @@ export const StyledTable = styled(Table).attrs(() => ({ bordered: true, hover: true, variant: "dark", - className: "m-0", + className: "mx-0 mt-0 mb-5", }))``; export const LoadingSpinner = styled(Spinner).attrs(() => ({ @@ -25,3 +25,7 @@ export const DeleteButton = styled(Button).attrs(() => ({ export const RowContainer = styled.td.attrs(() => ({ className: "p-3 d-flex", }))``; + +export const StyledNewEditionButton = styled(Button).attrs(() => ({ + className: "ms-auto my-3", +}))``; diff --git a/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx b/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx new file mode 100644 index 000000000..93fb5bff7 --- /dev/null +++ b/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx @@ -0,0 +1,6 @@ +/** + * Page to create a new edition + */ +export default function CreateEditionPage() { + return
                                      Here be create edition form
                                      ; +} diff --git a/frontend/src/views/CreateEditionPage/index.ts b/frontend/src/views/CreateEditionPage/index.ts new file mode 100644 index 000000000..730e93bb5 --- /dev/null +++ b/frontend/src/views/CreateEditionPage/index.ts @@ -0,0 +1 @@ +export { default } from "./CreateEditionPage"; diff --git a/frontend/src/views/EditionsPage/EditionsPage.tsx b/frontend/src/views/EditionsPage/EditionsPage.tsx index 1a17799eb..f2f7dc0a4 100644 --- a/frontend/src/views/EditionsPage/EditionsPage.tsx +++ b/frontend/src/views/EditionsPage/EditionsPage.tsx @@ -1,14 +1,18 @@ -import { EditionsTable } from "../../components/EditionsPage"; +import { EditionsTable, NewEditionButton } from "../../components/EditionsPage"; import { EditionsPageContainer } from "./styles"; +import { useNavigate } from "react-router-dom"; /** * Page where users can see all editions they can access, * and admins can delete editions. */ export default function EditionsPage() { + const navigate = useNavigate(); + return (

                                      Editions

                                      + navigate("/editions/new")} />
                                      ); From 0a3102bfd7e0e756f71ea61e8bea37c8fa73490e Mon Sep 17 00:00:00 2001 From: stijndcl Date: Tue, 12 Apr 2022 01:54:28 +0200 Subject: [PATCH 361/536] Add some styling to the button --- .../src/components/EditionsPage/NewEditionButton.tsx | 9 ++++++++- frontend/src/components/EditionsPage/styles.ts | 10 +++++++++- frontend/src/components/Navbar/Navbar.tsx | 7 ++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/EditionsPage/NewEditionButton.tsx b/frontend/src/components/EditionsPage/NewEditionButton.tsx index 0c2b1ed05..8fbc5d87e 100644 --- a/frontend/src/components/EditionsPage/NewEditionButton.tsx +++ b/frontend/src/components/EditionsPage/NewEditionButton.tsx @@ -1,4 +1,7 @@ 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"; interface Props { onClick: () => void; @@ -8,5 +11,9 @@ interface Props { * Button to create a new edition, redirects to the [[CreateEditionPage]]. */ export default function NewEditionButton({ onClick }: Props) { - return Create new edition; + return ( + + Create new edition + + ); } diff --git a/frontend/src/components/EditionsPage/styles.ts b/frontend/src/components/EditionsPage/styles.ts index c91fb2544..adc52230a 100644 --- a/frontend/src/components/EditionsPage/styles.ts +++ b/frontend/src/components/EditionsPage/styles.ts @@ -28,4 +28,12 @@ export const RowContainer = styled.td.attrs(() => ({ export const StyledNewEditionButton = styled(Button).attrs(() => ({ className: "ms-auto my-3", -}))``; +}))` + background-color: var(--osoc_green); + border-color: var(--osoc_green); + + &:hover { + background-color: var(--osoc_orange); + border-color: var(--osoc_orange); + } +`; diff --git a/frontend/src/components/Navbar/Navbar.tsx b/frontend/src/components/Navbar/Navbar.tsx index bc379c2fd..ec3ec37fa 100644 --- a/frontend/src/components/Navbar/Navbar.tsx +++ b/frontend/src/components/Navbar/Navbar.tsx @@ -36,7 +36,12 @@ export default function Navbar() { // a const match = matchPath({ path: "/editions/:editionId/*" }, location.pathname); // This is a TypeScript shortcut for 3 if-statements - const editionId = match && match.params && match.params.editionId; + let editionId = match && match.params && match.params.editionId; + + // Matched /editions/new path + if (editionId === "new") { + editionId = null; + } // If the current URL contains an edition, use that // if not (eg. /editions), check SessionStorage From 11d358ca6f07cc0261f60d803e5cea39663fd00e Mon Sep 17 00:00:00 2001 From: stijndcl Date: Tue, 12 Apr 2022 11:49:36 +0200 Subject: [PATCH 362/536] Fix styling, show pending page if pending --- frontend/src/Router.tsx | 7 ++++++- .../EditionsPage/DeleteEditionModal/DeleteEditionModal.css | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 8f4812a79..a275eebae 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -15,13 +15,14 @@ import { } from "./views"; import { ForbiddenPage, NotFoundPage } from "./views/errors"; import CreateEditionPage from "./views/CreateEditionPage"; +import { Role } from "./data/enums"; /** * Router component to render different pages depending on the current url. Renders * the [[VerifyingTokenPage]] if the bearer token is still being validated. */ export default function Router() { - const { isLoggedIn } = useAuth(); + const { isLoggedIn, role, editions } = useAuth(); return ( @@ -31,6 +32,10 @@ export default function Router() { {isLoggedIn === null ? ( // Busy verifying the access token + ) : isLoggedIn && role !== Role.ADMIN && editions.length === 0 ? ( + // If you are a coach but aren't part of any editions at all, you can't do + // anything in the application, so you shouldn't be able to access it either + ) : ( // Access token was checked, if it is invalid // then the will redirect to diff --git a/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.css b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.css index 57168ea9b..7108413f0 100644 --- a/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.css +++ b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.css @@ -22,4 +22,4 @@ .form-text { color: white; -} \ No newline at end of file +} From 5fe054cfde70007c274a376873c9fdbecbaaa731 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Tue, 12 Apr 2022 12:03:35 +0200 Subject: [PATCH 363/536] Hide nav components if not admin or verified --- .../EditionsPage/NewEditionButton.tsx | 9 ++++++ .../src/components/Navbar/EditionDropdown.tsx | 12 ++++++- frontend/src/components/Navbar/Navbar.tsx | 11 ++----- .../src/components/Navbar/UsersDropdown.tsx | 31 +++++++++++++++++++ 4 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 frontend/src/components/Navbar/UsersDropdown.tsx diff --git a/frontend/src/components/EditionsPage/NewEditionButton.tsx b/frontend/src/components/EditionsPage/NewEditionButton.tsx index 8fbc5d87e..5a482add7 100644 --- a/frontend/src/components/EditionsPage/NewEditionButton.tsx +++ b/frontend/src/components/EditionsPage/NewEditionButton.tsx @@ -2,6 +2,8 @@ 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"; interface Props { onClick: () => void; @@ -11,6 +13,13 @@ interface Props { * Button to create a new edition, redirects to the [[CreateEditionPage]]. */ export default function NewEditionButton({ onClick }: Props) { + const { role } = useAuth(); + + // Only admins can create new editions + if (role !== Role.ADMIN) { + return null; + } + return ( Create new edition diff --git a/frontend/src/components/Navbar/EditionDropdown.tsx b/frontend/src/components/Navbar/EditionDropdown.tsx index 6d9ef64fb..3be619264 100644 --- a/frontend/src/components/Navbar/EditionDropdown.tsx +++ b/frontend/src/components/Navbar/EditionDropdown.tsx @@ -15,7 +15,17 @@ export default function EditionDropdown(props: Props) { const navItems: React.ReactNode[] = []; const navigate = useNavigate(); - const currentEdition = getCurrentEdition(); + // 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; + } + + // 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]; /** * Change the route based on the edition diff --git a/frontend/src/components/Navbar/Navbar.tsx b/frontend/src/components/Navbar/Navbar.tsx index ec3ec37fa..2b72ba60d 100644 --- a/frontend/src/components/Navbar/Navbar.tsx +++ b/frontend/src/components/Navbar/Navbar.tsx @@ -1,14 +1,14 @@ import Container from "react-bootstrap/Container"; -import { BSNavbar, StyledDropdownItem } from "./styles"; +import { BSNavbar } from "./styles"; import { useAuth } from "../../contexts"; import Brand from "./Brand"; import Nav from "react-bootstrap/Nav"; -import NavDropdown from "react-bootstrap/NavDropdown"; import EditionDropdown from "./EditionDropdown"; import "./Navbar.css"; import LogoutButton from "./LogoutButton"; import { getCurrentEdition, setCurrentEdition } from "../../utils/session-storage/current-edition"; import { matchPath, useLocation } from "react-router-dom"; +import UsersDropdown from "./UsersDropdown"; /** * Navbar component displayed at the top of the screen. @@ -65,12 +65,7 @@ export default function Navbar() { Editions Projects Students - - Admins - - Coaches - - + diff --git a/frontend/src/components/Navbar/UsersDropdown.tsx b/frontend/src/components/Navbar/UsersDropdown.tsx new file mode 100644 index 000000000..e4a9b4ece --- /dev/null +++ b/frontend/src/components/Navbar/UsersDropdown.tsx @@ -0,0 +1,31 @@ +import { useAuth } from "../../contexts"; +import NavDropdown from "react-bootstrap/NavDropdown"; +import { StyledDropdownItem } from "./styles"; +import { Role } from "../../data/enums"; + +interface Props { + currentEdition: string; +} + +/** + * NavDropdown that links to the [[AdminsPage]] and [[UsersPage]]. + * This component is only rendered for admins. + */ +export default function UsersDropdown({ currentEdition }: Props) { + const { role } = useAuth(); + + // Only admins can see the dropdown because coaches can't + // access these pages anyway + if (role !== Role.ADMIN) { + return null; + } + + return ( + + Admins + + Coaches + + + ); +} From ca87d045860ee97797f3b902469414155583820a Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Tue, 12 Apr 2022 13:31:13 +0200 Subject: [PATCH 364/536] add nr of suggestions to a return model of student --- backend/src/app/logic/students.py | 27 ++++++++++++--- backend/src/app/schemas/students.py | 14 +++++++- backend/src/database/crud/suggestions.py | 5 +++ .../test_crud/test_suggestions.py | 34 +++++++++++++++++-- 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index 414bfc3ac..972f2db4a 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -1,11 +1,15 @@ +from ast import Return from sqlalchemy.orm import Session from sqlalchemy.orm.exc import NoResultFound from src.app.schemas.students import NewDecision -from src.database.crud.students import set_definitive_decision_on_student, delete_student, get_students, get_emails from src.database.crud.skills import get_skills_by_ids +from src.database.crud.students import set_definitive_decision_on_student, delete_student, get_students, get_emails +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.app.schemas.students import ReturnStudentList, ReturnStudent, CommonQueryParams, ReturnStudentMailList +from src.app.schemas.students import ( + ReturnStudentList, ReturnStudent, CommonQueryParams, ReturnStudentMailList, Student as StudentModel) def definitive_decision_on_student(db: Session, student: Student, decision: NewDecision) -> None: @@ -26,9 +30,22 @@ def get_students_search(db: Session, edition: Edition, commons: CommonQueryParam return ReturnStudentList(students=[]) else: skills = [] - students = get_students(db, edition, first_name=commons.first_name, - last_name=commons.last_name, alumni=commons.alumni, - student_coach=commons.student_coach, skills=skills) + students_orm = get_students(db, edition, first_name=commons.first_name, + last_name=commons.last_name, alumni=commons.alumni, + student_coach=commons.student_coach, skills=skills) + + students: list[StudentModel] = [] + for student in students_orm: + students.append(student) + 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 = {'yes': nr_of_yes_suggestions, + 'no': nr_of_no_suggestions, + 'maybe': nr_of_maybe_suggestions} return ReturnStudentList(students=students) diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index d50def337..4752b2f77 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -1,5 +1,6 @@ from datetime import datetime from fastapi import Query +from pydantic import Field from src.app.schemas.webhooks import CamelCaseModel from src.database.enums import DecisionEnum @@ -11,6 +12,15 @@ class NewDecision(CamelCaseModel): decision: DecisionEnum +class Suggestions(CamelCaseModel): + """ + Model to represent to number of suggestions organised by type + """ + yes: int + maybe: int + no: int + + class Student(CamelCaseModel): """ Model to represent a Student @@ -23,11 +33,13 @@ class Student(CamelCaseModel): email_address: str phone_number: str alumni: bool - decision: DecisionEnum + decision: DecisionEnum = Field( + DecisionEnum.UNDECIDED, alias="finalDecision") wants_to_be_student_coach: bool edition_id: int skills: list[Skill] + nr_of_suggestions: Suggestions = None class Config: """Set to ORM mode""" diff --git a/backend/src/database/crud/suggestions.py b/backend/src/database/crud/suggestions.py index 279e7b555..4af00f6db 100644 --- a/backend/src/database/crud/suggestions.py +++ b/backend/src/database/crud/suggestions.py @@ -36,3 +36,8 @@ def update_suggestion(db: Session, suggestion: Suggestion, decision: DecisionEnu suggestion.suggestion = decision suggestion.argumentation = argumentation db.commit() + + +def get_suggestions_of_student_by_type(db: Session, student_id: int | None, type: 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).all() diff --git a/backend/tests/test_database/test_crud/test_suggestions.py b/backend/tests/test_database/test_crud/test_suggestions.py index b179dd919..995e4a800 100644 --- a/backend/tests/test_database/test_crud/test_suggestions.py +++ b/backend/tests/test_database/test_crud/test_suggestions.py @@ -5,10 +5,12 @@ from src.database.models import Suggestion, Student, User, Edition, Skill -from src.database.crud.suggestions import ( create_suggestion, get_suggestions_of_student, - get_suggestion_by_id, delete_suggestion, update_suggestion ) +from src.database.crud.suggestions import (create_suggestion, get_suggestions_of_student, + get_suggestion_by_id, delete_suggestion, update_suggestion, + get_suggestions_of_student_by_type) from src.database.enums import DecisionEnum + @pytest.fixture def database_with_data(database_session: Session): """A function to fill the database with fake data that can easly be used when testing""" @@ -249,3 +251,31 @@ def test_update_suggestion(database_with_data: Session): Suggestion.coach == user).where(Suggestion.student_id == student.student_id).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): + """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, + DecisionEnum.MAYBE, "Idk if it's good student") + 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, + student.student_id, DecisionEnum.NO, "This is not a good student") + suggestions_student_yes = get_suggestions_of_student_by_type( + database_with_data, student.student_id, DecisionEnum.YES) + suggestions_student_no = get_suggestions_of_student_by_type( + database_with_data, student.student_id, DecisionEnum.NO) + suggestions_student_maybe = 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 + assert len(suggestions_student_maybe) == 1 From 95cf4a34c31144ef142d14dcaaa81c3864c42502 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Tue, 12 Apr 2022 13:32:23 +0200 Subject: [PATCH 365/536] deleted unused import --- .../test_students/test_suggestions/test_suggestions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 9ee60ede8..5c834bf7f 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 @@ -2,8 +2,7 @@ from sqlalchemy.orm import Session from starlette import status from src.database.enums import DecisionEnum -from src.database.models import Suggestion, Student, User, Edition, Skill, AuthEmail -from src.app.logic.security import get_password_hash +from src.database.models import Suggestion, Student, User, Edition, Skill from tests.utils.authorization import AuthClient From 847613d41dfe5c0bc96b890ab28da2b641140c26 Mon Sep 17 00:00:00 2001 From: Seppe <57944475+SeppeM8@users.noreply.github.com> Date: Tue, 12 Apr 2022 14:41:27 +0200 Subject: [PATCH 366/536] Update backend/src/database/crud/users.py Co-authored-by: Stijn De Clercq <60451863+stijndcl@users.noreply.github.com> --- backend/src/database/crud/users.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 3a4a36847..1fd65cc32 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -31,12 +31,12 @@ def get_users_filtered( page: int = 0 ): """ - Get users and filter by optional parameter: - - admin: only return admins / only return non-admins - - edition_name: only return users who are coach of the given edition - - exclude_edition_name: only return users who are not coach of the given edition - - name: a string which the user's name must contain - - page: the page to return + 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. """ From b7411a01c39a6f40706c0581b18affd6c2798311 Mon Sep 17 00:00:00 2001 From: Seppe <57944475+SeppeM8@users.noreply.github.com> Date: Tue, 12 Apr 2022 14:41:33 +0200 Subject: [PATCH 367/536] Update backend/src/database/crud/users.py Co-authored-by: Stijn De Clercq <60451863+stijndcl@users.noreply.github.com> --- backend/src/database/crud/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 1fd65cc32..41e3fcb05 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -41,7 +41,7 @@ def get_users_filtered( Note: When the admin parameter is set, edition_name and exclude_edition_name will be ignored. """ - query = db.query(User)\ + query = db.query(User) if name is not None: query = query.where(User.name.contains(name)) From 5dde725f8c3bf039f8d5e7dada29b3e62ebbb72a Mon Sep 17 00:00:00 2001 From: Seppe <57944475+SeppeM8@users.noreply.github.com> Date: Tue, 12 Apr 2022 14:44:01 +0200 Subject: [PATCH 368/536] Update backend/src/database/crud/users.py Co-authored-by: Stijn De Clercq <60451863+stijndcl@users.noreply.github.com> --- backend/src/database/crud/users.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 41e3fcb05..30407f2fa 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -47,10 +47,7 @@ def get_users_filtered( query = query.where(User.name.contains(name)) if admin is not None: - if admin: - query = query.filter(User.admin) - else: - query = query.filter(~User.admin) + query = query.filter(User.admin.is_(admin)) # If admin parameter is set, edition & exclude_edition is ignored return paginate(query, page).all() From cf9742a97a93953d2b14456c395d487cb1f833ae Mon Sep 17 00:00:00 2001 From: stijndcl Date: Tue, 12 Apr 2022 15:31:05 +0200 Subject: [PATCH 369/536] Smart redirect for editions dropdown --- .../DeleteEditionModal/DeleteEditionModal.tsx | 5 +-- .../src/components/Navbar/EditionDropdown.tsx | 13 ++++--- frontend/src/components/Navbar/Navbar.tsx | 2 +- frontend/src/contexts/auth-context.tsx | 2 +- frontend/src/utils/index.ts | 2 ++ frontend/src/utils/logic/index.ts | 1 + frontend/src/utils/logic/routes.ts | 35 +++++++++++++++++++ frontend/src/utils/session-storage/index.ts | 1 + 8 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 frontend/src/utils/logic/index.ts create mode 100644 frontend/src/utils/logic/routes.ts create mode 100644 frontend/src/utils/session-storage/index.ts diff --git a/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx index 04cd13b08..e8f98f4da 100644 --- a/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx +++ b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx @@ -7,10 +7,7 @@ import Form from "react-bootstrap/Form"; import InfoMessage from "./InfoMessage"; import Spinner from "react-bootstrap/Spinner"; import { deleteEdition } from "../../../utils/api/editions"; -import { - getCurrentEdition, - setCurrentEdition, -} from "../../../utils/session-storage/current-edition"; +import { getCurrentEdition, setCurrentEdition } from "../../../utils/session-storage"; interface Props { edition: Edition; diff --git a/frontend/src/components/Navbar/EditionDropdown.tsx b/frontend/src/components/Navbar/EditionDropdown.tsx index 3be619264..3a108b1ea 100644 --- a/frontend/src/components/Navbar/EditionDropdown.tsx +++ b/frontend/src/components/Navbar/EditionDropdown.tsx @@ -1,8 +1,9 @@ import React from "react"; import NavDropdown from "react-bootstrap/NavDropdown"; import { StyledDropdownItem } from "./styles"; -import { useNavigate } from "react-router-dom"; -import { getCurrentEdition } from "../../utils/session-storage/current-edition"; +import { useLocation, useNavigate } from "react-router-dom"; +import { getCurrentEdition, setCurrentEdition } from "../../utils/session-storage"; +import { getBestRedirect } from "../../utils/logic"; interface Props { editions: string[]; @@ -13,6 +14,7 @@ interface Props { */ export default function EditionDropdown(props: Props) { const navItems: React.ReactNode[] = []; + const location = useLocation(); const navigate = useNavigate(); // User can't access any editions yet, no point in rendering the dropdown either @@ -33,9 +35,10 @@ export default function EditionDropdown(props: Props) { * only be used in React components */ function handleSelect(edition: string) { - // TODO: Navigate to the most specific route possible for QOL? - // eg. /editions/old_id/students/:id => /editions/new_id/students, etc - navigate(`/editions/${edition}`); + const destination = getBestRedirect(location, edition); + setCurrentEdition(edition); + + navigate(destination); } // Load dropdown items dynamically diff --git a/frontend/src/components/Navbar/Navbar.tsx b/frontend/src/components/Navbar/Navbar.tsx index 2b72ba60d..45aec8adf 100644 --- a/frontend/src/components/Navbar/Navbar.tsx +++ b/frontend/src/components/Navbar/Navbar.tsx @@ -6,7 +6,7 @@ import Nav from "react-bootstrap/Nav"; import EditionDropdown from "./EditionDropdown"; import "./Navbar.css"; import LogoutButton from "./LogoutButton"; -import { getCurrentEdition, setCurrentEdition } from "../../utils/session-storage/current-edition"; +import { getCurrentEdition, setCurrentEdition } from "../../utils/session-storage"; import { matchPath, useLocation } from "react-router-dom"; import UsersDropdown from "./UsersDropdown"; diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx index c8f4149fc..f4aee5dbd 100644 --- a/frontend/src/contexts/auth-context.tsx +++ b/frontend/src/contexts/auth-context.tsx @@ -4,7 +4,7 @@ import React, { useContext, ReactNode, useState } from "react"; import { getToken, setToken as setTokenInStorage } from "../utils/local-storage"; import { User } from "../data/interfaces"; import { setBearerToken } from "../utils/api"; -import { setCurrentEdition } from "../utils/session-storage/current-edition"; +import { setCurrentEdition } from "../utils/session-storage"; /** * Interface that holds the data stored in the AuthContext. diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts index 464de6bdf..b478e8580 100644 --- a/frontend/src/utils/index.ts +++ b/frontend/src/utils/index.ts @@ -1,2 +1,4 @@ export * as Api from "./api"; export * as LocalStorage from "./local-storage"; +export * as Logic from "./logic"; +export * as SessionStorage from "./session-storage"; diff --git a/frontend/src/utils/logic/index.ts b/frontend/src/utils/logic/index.ts new file mode 100644 index 000000000..dfb0a10c0 --- /dev/null +++ b/frontend/src/utils/logic/index.ts @@ -0,0 +1 @@ +export { getBestRedirect } from "./routes"; diff --git a/frontend/src/utils/logic/routes.ts b/frontend/src/utils/logic/routes.ts new file mode 100644 index 000000000..09c54ae95 --- /dev/null +++ b/frontend/src/utils/logic/routes.ts @@ -0,0 +1,35 @@ +import { Location, matchPath } from "react-router-dom"; + +/** + * Get the best matching route to redirect to + * Boils down to the most-specific route that can be used across editions + */ +export function getBestRedirect(location: Location, editionName: string): string { + // All /student/X routes should go to /students + if (matchPath({ path: "/editions/:edition/students/*" }, location.pathname)) { + return `/editions/${editionName}/students`; + } + + // All /project/X routes should go to /projects + if (matchPath({ path: "/editions/:edition/projects/*" }, location.pathname)) { + return `/editions/${editionName}/projects`; + } + + // /admins can stay where it is + if (matchPath({ path: "/admins" }, location.pathname)) { + return "/admins"; + } + + // All /users/X routes should go to /users + if (matchPath({ path: "/editions/:edition/users/*" }, location.pathname)) { + return `/editions/${editionName}/users`; + } + + // Being on the edition-specific page should keep you there + if (matchPath({ path: "/editions/:edition" }, location.pathname)) { + return `/editions/${editionName}`; + } + + // All the rest: go to /editions + return "/editions"; +} diff --git a/frontend/src/utils/session-storage/index.ts b/frontend/src/utils/session-storage/index.ts new file mode 100644 index 000000000..2b2ae5a58 --- /dev/null +++ b/frontend/src/utils/session-storage/index.ts @@ -0,0 +1 @@ +export { getCurrentEdition, setCurrentEdition } from "./current-edition"; From 48fb9170a0d21ddea02f1edd5480fb89e60ea45a Mon Sep 17 00:00:00 2001 From: stijndcl Date: Tue, 12 Apr 2022 15:42:06 +0200 Subject: [PATCH 370/536] Add tests --- .../src/components/Navbar/EditionDropdown.tsx | 2 +- frontend/src/utils/logic/routes.test.ts | 39 +++++++++++++++++++ frontend/src/utils/logic/routes.ts | 14 +++---- 3 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 frontend/src/utils/logic/routes.test.ts diff --git a/frontend/src/components/Navbar/EditionDropdown.tsx b/frontend/src/components/Navbar/EditionDropdown.tsx index 3a108b1ea..c6e7ceb78 100644 --- a/frontend/src/components/Navbar/EditionDropdown.tsx +++ b/frontend/src/components/Navbar/EditionDropdown.tsx @@ -35,7 +35,7 @@ export default function EditionDropdown(props: Props) { * only be used in React components */ function handleSelect(edition: string) { - const destination = getBestRedirect(location, edition); + const destination = getBestRedirect(location.pathname, edition); setCurrentEdition(edition); navigate(destination); diff --git a/frontend/src/utils/logic/routes.test.ts b/frontend/src/utils/logic/routes.test.ts new file mode 100644 index 000000000..0f370b567 --- /dev/null +++ b/frontend/src/utils/logic/routes.test.ts @@ -0,0 +1,39 @@ +import { getBestRedirect } 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 stays there", () => { + expect(getBestRedirect("/editions/old/students", "new")).toEqual("/editions/new/students"); + expect(getBestRedirect("/editions/old/students/", "new")).toEqual("/editions/new/students"); +}); + +test("/students/:id goes to /students", () => { + expect(getBestRedirect("/editions/old/students/id", "new")).toEqual("/editions/new/students"); +}); + +test("/projects stays there", () => { + expect(getBestRedirect("/editions/old/projects", "new")).toEqual("/editions/new/projects"); + expect(getBestRedirect("/editions/old/projects/", "new")).toEqual("/editions/new/projects"); +}); + +test("/projects/:id goes to /projects", () => { + expect(getBestRedirect("/editions/old/projects/id", "new")).toEqual("/editions/new/projects"); +}); + +test("/users stays there", () => { + expect(getBestRedirect("/editions/old/users", "new")).toEqual("/editions/new/users"); + expect(getBestRedirect("/editions/old/users/", "new")).toEqual("/editions/new/users"); +}); + +test("/admins stays there", () => { + expect(getBestRedirect("/admins", "new")).toEqual("/admins"); + expect(getBestRedirect("/admins/", "new")).toEqual("/admins"); +}); + +test("/editions stays there", () => { + expect(getBestRedirect("/editions", "new")).toEqual("/editions"); + expect(getBestRedirect("/editions/", "new")).toEqual("/editions"); +}); diff --git a/frontend/src/utils/logic/routes.ts b/frontend/src/utils/logic/routes.ts index 09c54ae95..68260dc15 100644 --- a/frontend/src/utils/logic/routes.ts +++ b/frontend/src/utils/logic/routes.ts @@ -1,32 +1,32 @@ -import { Location, matchPath } from "react-router-dom"; +import { matchPath } from "react-router-dom"; /** * Get the best matching route to redirect to * Boils down to the most-specific route that can be used across editions */ -export function getBestRedirect(location: Location, editionName: string): string { +export function getBestRedirect(location: string, editionName: string): string { // All /student/X routes should go to /students - if (matchPath({ path: "/editions/:edition/students/*" }, location.pathname)) { + if (matchPath({ path: "/editions/:edition/students/*" }, location)) { return `/editions/${editionName}/students`; } // All /project/X routes should go to /projects - if (matchPath({ path: "/editions/:edition/projects/*" }, location.pathname)) { + if (matchPath({ path: "/editions/:edition/projects/*" }, location)) { return `/editions/${editionName}/projects`; } // /admins can stay where it is - if (matchPath({ path: "/admins" }, location.pathname)) { + if (matchPath({ path: "/admins" }, location)) { return "/admins"; } // All /users/X routes should go to /users - if (matchPath({ path: "/editions/:edition/users/*" }, location.pathname)) { + if (matchPath({ path: "/editions/:edition/users/*" }, location)) { return `/editions/${editionName}/users`; } // Being on the edition-specific page should keep you there - if (matchPath({ path: "/editions/:edition" }, location.pathname)) { + if (matchPath({ path: "/editions/:edition" }, location)) { return `/editions/${editionName}`; } From 781b9e9a65ea095c820489b2eff77123493d031a Mon Sep 17 00:00:00 2001 From: Ward Meersman <48222993+WardM99@users.noreply.github.com> Date: Tue, 12 Apr 2022 15:44:16 +0200 Subject: [PATCH 371/536] Apply suggestions from code review Co-authored-by: Stijn De Clercq <60451863+stijndcl@users.noreply.github.com> --- backend/src/app/logic/suggestions.py | 9 ++------- backend/src/database/crud/suggestions.py | 1 + 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py index 69c9a61ae..6edbac25c 100644 --- a/backend/src/app/logic/suggestions.py +++ b/backend/src/app/logic/suggestions.py @@ -31,9 +31,7 @@ def remove_suggestion(db: Session, suggestion: Suggestion, user: User) -> None: Delete a suggestion Admins can delete all suggestions, coaches only their own suggestions """ - if user.admin: - delete_suggestion(db, suggestion) - elif suggestion.coach == user: + if user.admin or suggestion.coach == user: delete_suggestion(db, suggestion) else: raise MissingPermissionsException @@ -44,10 +42,7 @@ def change_suggestion(db: Session, new_suggestion: NewSuggestion, suggestion: Su Update a suggestion Admins can update all suggestions, coaches only their own suggestions """ - if user.admin: - update_suggestion( - db, suggestion, new_suggestion.suggestion, new_suggestion.argumentation) - elif suggestion.coach == user: + if user.admin or suggestion.coach == user: update_suggestion( db, suggestion, new_suggestion.suggestion, new_suggestion.argumentation) else: diff --git a/backend/src/database/crud/suggestions.py b/backend/src/database/crud/suggestions.py index 279e7b555..fb340714f 100644 --- a/backend/src/database/crud/suggestions.py +++ b/backend/src/database/crud/suggestions.py @@ -29,6 +29,7 @@ def get_suggestion_by_id(db: Session, suggestion_id: int) -> Suggestion: def delete_suggestion(db: Session, suggestion: Suggestion) -> None: """Delete a suggestion from the database""" db.delete(suggestion) + db.commit() def update_suggestion(db: Session, suggestion: Suggestion, decision: DecisionEnum, argumentation: str) -> None: From a1c752d86e3a5bf3f9cd0575cc6017d36b25a4fa Mon Sep 17 00:00:00 2001 From: beguille Date: Tue, 12 Apr 2022 16:00:47 +0200 Subject: [PATCH 372/536] old projects and project-roles are removable --- backend/src/app/routers/editions/projects/projects.py | 2 +- .../app/routers/editions/projects/students/projects_students.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index 6167a8f97..94449a47f 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -46,7 +46,7 @@ async def get_conflicts(db: Session = Depends(get_session), edition: Edition = D @projects_router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, - dependencies=[Depends(require_admin), Depends(get_latest_edition)]) + dependencies=[Depends(require_admin)]) async def delete_project(project_id: int, db: Session = Depends(get_session)): """ Delete a specific project. 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 0edc42d02..1674da94c 100644 --- a/backend/src/app/routers/editions/projects/students/projects_students.py +++ b/backend/src/app/routers/editions/projects/students/projects_students.py @@ -15,7 +15,7 @@ @project_students_router.delete("/{student_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, - dependencies=[Depends(require_coach), Depends(get_latest_edition)]) + dependencies=[Depends(require_coach)]) async def remove_student_from_project(student_id: int, db: Session = Depends(get_session), project: Project = Depends(get_project)): """ From dd0eba66abd8b634cf39a9b2f142b95a26447789 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Tue, 12 Apr 2022 16:29:46 +0200 Subject: [PATCH 373/536] uses the class instead of json --- backend/src/app/logic/students.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index 972f2db4a..bc73e408d 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -9,7 +9,8 @@ from src.database.enums import DecisionEnum from src.database.models import Edition, Student, Skill, DecisionEmail from src.app.schemas.students import ( - ReturnStudentList, ReturnStudent, CommonQueryParams, ReturnStudentMailList, Student as StudentModel) + ReturnStudentList, ReturnStudent, CommonQueryParams, ReturnStudentMailList, + Student as StudentModel, Suggestions as SuggestionsModel) def definitive_decision_on_student(db: Session, student: Student, decision: NewDecision) -> None: @@ -43,9 +44,8 @@ def get_students_search(db: Session, edition: Edition, commons: CommonQueryParam 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 = {'yes': nr_of_yes_suggestions, - 'no': nr_of_no_suggestions, - 'maybe': nr_of_maybe_suggestions} + students[-1].nr_of_suggestions = SuggestionsModel( + yes=nr_of_yes_suggestions, no=nr_of_no_suggestions, maybe=nr_of_maybe_suggestions) return ReturnStudentList(students=students) From 118d4d0c83b09bc0f77fce59b988e961d67f439d Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Tue, 12 Apr 2022 17:10:25 +0200 Subject: [PATCH 374/536] delete empty migrations --- ...fa0af0a_added_to_the_right_side_of_the_.py | 28 ------------------- .../54119dfdbb44_added_cascade_delete.py | 28 ------------------- 2 files changed, 56 deletions(-) delete mode 100644 backend/migrations/versions/32e41fa0af0a_added_to_the_right_side_of_the_.py delete mode 100644 backend/migrations/versions/54119dfdbb44_added_cascade_delete.py diff --git a/backend/migrations/versions/32e41fa0af0a_added_to_the_right_side_of_the_.py b/backend/migrations/versions/32e41fa0af0a_added_to_the_right_side_of_the_.py deleted file mode 100644 index 96ebdcd6f..000000000 --- a/backend/migrations/versions/32e41fa0af0a_added_to_the_right_side_of_the_.py +++ /dev/null @@ -1,28 +0,0 @@ -"""added to the right side of the relationship this time - -Revision ID: 32e41fa0af0a -Revises: 54119dfdbb44 -Create Date: 2022-04-10 15:15:05.204273 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '32e41fa0af0a' -down_revision = '54119dfdbb44' -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/54119dfdbb44_added_cascade_delete.py b/backend/migrations/versions/54119dfdbb44_added_cascade_delete.py deleted file mode 100644 index d2514f84d..000000000 --- a/backend/migrations/versions/54119dfdbb44_added_cascade_delete.py +++ /dev/null @@ -1,28 +0,0 @@ -"""added cascade delete - -Revision ID: 54119dfdbb44 -Revises: 1862d7dea4cc -Create Date: 2022-04-10 15:10:51.251243 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '54119dfdbb44' -down_revision = '1862d7dea4cc' -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 ### From 5fcedf8d8d280338dac1da3544d6cdbcf1a561e3 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Tue, 12 Apr 2022 17:33:35 +0200 Subject: [PATCH 375/536] delete usseless import --- backend/src/app/logic/students.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index bc73e408d..cb9e634f1 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -1,4 +1,3 @@ -from ast import Return from sqlalchemy.orm import Session from sqlalchemy.orm.exc import NoResultFound From 13fadddaa6afc80b52f68b375e6ad0bc3169ae52 Mon Sep 17 00:00:00 2001 From: Francis Date: Wed, 13 Apr 2022 00:00:24 +0200 Subject: [PATCH 376/536] refresh tokens start --- backend/src/app/logic/security.py | 26 +++++++++++------- backend/src/app/routers/login/login.py | 37 +++++++++++++++++++------- backend/src/app/schemas/login.py | 5 ++-- backend/src/app/utils/dependencies.py | 3 ++- 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/backend/src/app/logic/security.py b/backend/src/app/logic/security.py index 2320ebc08..dc990de1a 100644 --- a/backend/src/app/logic/security.py +++ b/backend/src/app/logic/security.py @@ -8,26 +8,32 @@ from src.app.exceptions.authentication import InvalidCredentialsException from src.database import models from src.database.crud.users import get_user_by_email +from src.database.models import User # Configuration + ALGORITHM = "HS256" pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str: - """Encode the user data with an expire timestamp to create the token""" - to_encode = data.copy() +def create_tokens(user: User) -> tuple[str, str]: + """ + Create an access token and refresh token. - if expires_delta is not None: - expire = datetime.utcnow() + expires_delta - else: - expire = datetime.utcnow() + timedelta(hours=settings.ACCESS_TOKEN_EXPIRE_HOURS) + Returns: (access_token, refresh_token) + """ + return ( + _create_token({"type": "access", "sub": str(user.user_id)}, settings.ACCESS_TOKEN_EXPIRE_MINUTES), + _create_token({"type": "refresh", "sub": str(user.user_id)}, settings.REFRESH_TOKEN_EXPIRE_MINUTES) + ) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM) - return encoded_jwt +def _create_token(data: dict, expires_delta: int) -> str: + """Encode the user data with an expiry timestamp to create the token""" + # The 'exp' key here is extremely important. if this key changes expiry will not be checked. + data["exp"] = datetime.utcnow() + timedelta(minutes=expires_delta) + return jwt.encode(data, settings.SECRET_KEY, algorithm=ALGORITHM) def verify_password(plain_password: str, hashed_password: str) -> bool: diff --git a/backend/src/app/routers/login/login.py b/backend/src/app/routers/login/login.py index fe96a3091..819cd3f1c 100644 --- a/backend/src/app/routers/login/login.py +++ b/backend/src/app/routers/login/login.py @@ -1,19 +1,18 @@ -from datetime import timedelta - import sqlalchemy.exc from fastapi import APIRouter from fastapi import Depends from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.orm import Session -import settings from src.app.exceptions.authentication import InvalidCredentialsException -from src.app.logic.security import authenticate_user, create_access_token +from src.app.logic.security import authenticate_user, create_tokens from src.app.logic.users import get_user_editions from src.app.routers.tags import Tags -from src.app.schemas.login import Token +from src.app.schemas.login import Token, UserData from src.app.schemas.users import user_model_to_schema +from src.app.utils.dependencies import get_current_active_user from src.database.database import get_session +from src.database.models import User login_router = APIRouter(prefix="/login", tags=[Tags.LOGIN]) @@ -29,12 +28,30 @@ async def login_for_access_token(db: Session = Depends(get_session), # be a 401 instead of a 404 raise InvalidCredentialsException() from not_found - access_token_expires = timedelta(hours=settings.ACCESS_TOKEN_EXPIRE_HOURS) - access_token = create_access_token( - data={"sub": str(user.user_id)}, expires_delta=access_token_expires + access_token, refresh_token = create_tokens(user) + + user_data: dict = user_model_to_schema(user).__dict__ + user_data["editions"] = get_user_editions(db, user) + + return Token( + access_token=access_token, + refresh_token=refresh_token, + token_type="bearer", + user=UserData(**user_data) ) - user_data = user_model_to_schema(user).__dict__ + +@login_router.post("/refresh", response_model=Token) +async def refresh_access_token(db: Session = Depends(get_session), user: User = Depends(get_current_active_user)): + """Return a new access & refresh token using on the old refresh token""" + access_token, refresh_token = create_tokens(user) + + user_data: dict = user_model_to_schema(user).__dict__ user_data["editions"] = get_user_editions(db, user) - return {"access_token": access_token, "token_type": "bearer", "user": user_data} + return Token( + access_token=access_token, + refresh_token=refresh_token, + token_type="bearer", + user=UserData(**user_data) + ) diff --git a/backend/src/app/schemas/login.py b/backend/src/app/schemas/login.py index 2ae9ed152..b7e735e73 100644 --- a/backend/src/app/schemas/login.py +++ b/backend/src/app/schemas/login.py @@ -1,5 +1,5 @@ from src.app.schemas.users import User -from src.app.schemas.utils import CamelCaseModel +from src.app.schemas.utils import BaseModel class UserData(User): @@ -7,10 +7,11 @@ class UserData(User): editions: list[str] = [] -class Token(CamelCaseModel): +class Token(BaseModel): """Token generated after login Also contains data about the User to set permissions in frontend """ access_token: str + refresh_token: str token_type: str user: UserData diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index 55b2eab3f..805554ad9 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -21,13 +21,14 @@ def get_edition(edition_name: str, database: Session = Depends(get_session)) -> return get_edition_by_name(database, edition_name) -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login/token") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login/token") async def get_current_active_user(db: Session = Depends(get_session), token: str = Depends(oauth2_scheme)) -> User: """Check which user is making a request by decoding its token This function is used as a dependency for other functions """ + # TODO: check type of token. try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM]) user_id: int | None = payload.get("sub") From c46cf999875d131f5eabbcb64fc6652bcfa8857b Mon Sep 17 00:00:00 2001 From: Francis Date: Wed, 13 Apr 2022 10:39:03 +0200 Subject: [PATCH 377/536] commit settings --- backend/settings.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/settings.py b/backend/settings.py index 7e5b72a96..a37091f90 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -3,7 +3,6 @@ from environs import Env import enum - env = Env() # Read the .env file @@ -31,10 +30,13 @@ """JWT token key""" SECRET_KEY: str = env.str("SECRET_KEY", "4d16a9cc83d74144322e893c879b5f639088c15dc1606b11226abbd7e97f5ee5") -ACCESS_TOKEN_EXPIRE_HOURS: int = env.int("ACCESS_TOKEN_EXPIRE_HOURS", 168) +ACCESS_TOKEN_EXPIRE_MINUTES: int = env.int("ACCESS_TOKEN_EXPIRE_MINUTES", 1) +REFRESH_TOKEN_EXPIRE_MINUTES: int = env.int("REFRESH_TOKEN_EXPIRE_MINUTES", 2) + """Frontend""" FRONTEND_URL: str = env.str("FRONTEND_URL", "http://localhost:3000") + @enum.unique class FormMapping(enum.Enum): FIRST_NAME = "question_3ExXkL" @@ -43,7 +45,7 @@ class FormMapping(enum.Enum): PREFERRED_NAME = "question_3jlya9" EMAIL = "question_nW8NOQ" PHONE_NUMBER = "question_mea6qo" - #CV = "question_wa26Qy" + # CV = "question_wa26Qy" STUDENT_COACH = "question_wz7qEE" UNKNOWN = None # Returned when no specific question can be matched From 35901b862d4f5a1b480e732020bdbf32c7891e91 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Wed, 13 Apr 2022 12:01:49 +0200 Subject: [PATCH 378/536] differentiate between tokens --- backend/src/app/exceptions/authentication.py | 8 +++++ backend/src/app/exceptions/handlers.py | 11 +++++- backend/src/app/logic/security.py | 11 ++++-- backend/src/app/routers/login/login.py | 8 +++-- backend/src/app/utils/dependencies.py | 34 ++++++++++++++----- .../tests/utils/authorization/auth_client.py | 8 ++--- 6 files changed, 60 insertions(+), 20 deletions(-) diff --git a/backend/src/app/exceptions/authentication.py b/backend/src/app/exceptions/authentication.py index 4eb22b94c..f7bf55815 100644 --- a/backend/src/app/exceptions/authentication.py +++ b/backend/src/app/exceptions/authentication.py @@ -22,3 +22,11 @@ class MissingPermissionsException(ValueError): when their application is still pending, and they haven't been accepted yet """ + + +class WrongTokenTypeException(ValueError): + """ + Exception raised when a request to a private route is made with a + valid jwt token, but a wrong token type. eg: trying to authenticate + using a refresh token + """ \ No newline at end of file diff --git a/backend/src/app/exceptions/handlers.py b/backend/src/app/exceptions/handlers.py index f9ba95120..1c50ffde2 100644 --- a/backend/src/app/exceptions/handlers.py +++ b/backend/src/app/exceptions/handlers.py @@ -4,7 +4,9 @@ from pydantic import ValidationError from starlette import status -from .authentication import ExpiredCredentialsException, InvalidCredentialsException, MissingPermissionsException +from .authentication import ( + ExpiredCredentialsException, InvalidCredentialsException, + MissingPermissionsException, WrongTokenTypeException) from .editions import DuplicateInsertException from .parsing import MalformedUUIDError from .projects import StudentInConflictException, FailedToAddProjectRoleException @@ -94,3 +96,10 @@ def failed_to_add_project_role_exception(_request: Request, _exception: FailedTo status_code=status.HTTP_400_BAD_REQUEST, content={'message': 'Something went wrong while adding this student to the project'} ) + + @app.exception_handler(WrongTokenTypeException) + async def wrong_token_type_exception(_request: Request, _exception: WrongTokenTypeException): + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={'message': 'U used the wrong token to access this resource.'} + ) \ No newline at end of file diff --git a/backend/src/app/logic/security.py b/backend/src/app/logic/security.py index dc990de1a..73d48b417 100644 --- a/backend/src/app/logic/security.py +++ b/backend/src/app/logic/security.py @@ -1,3 +1,4 @@ +import enum from datetime import timedelta, datetime from jose import jwt @@ -17,6 +18,12 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +@enum.unique +class TokenType(enum.Enum): + ACCESS = "access" + REFRESH = "refresh" + + def create_tokens(user: User) -> tuple[str, str]: """ Create an access token and refresh token. @@ -24,8 +31,8 @@ def create_tokens(user: User) -> tuple[str, str]: Returns: (access_token, refresh_token) """ return ( - _create_token({"type": "access", "sub": str(user.user_id)}, settings.ACCESS_TOKEN_EXPIRE_MINUTES), - _create_token({"type": "refresh", "sub": str(user.user_id)}, settings.REFRESH_TOKEN_EXPIRE_MINUTES) + _create_token({"type": TokenType.ACCESS.value, "sub": str(user.user_id)}, settings.ACCESS_TOKEN_EXPIRE_MINUTES), + _create_token({"type": TokenType.REFRESH.value, "sub": str(user.user_id)}, settings.REFRESH_TOKEN_EXPIRE_MINUTES) ) diff --git a/backend/src/app/routers/login/login.py b/backend/src/app/routers/login/login.py index 819cd3f1c..0dbc63da0 100644 --- a/backend/src/app/routers/login/login.py +++ b/backend/src/app/routers/login/login.py @@ -10,7 +10,7 @@ from src.app.routers.tags import Tags from src.app.schemas.login import Token, UserData from src.app.schemas.users import user_model_to_schema -from src.app.utils.dependencies import get_current_active_user +from src.app.utils.dependencies import get_current_active_user, get_user_from_refresh_token from src.database.database import get_session from src.database.models import User @@ -42,8 +42,10 @@ async def login_for_access_token(db: Session = Depends(get_session), @login_router.post("/refresh", response_model=Token) -async def refresh_access_token(db: Session = Depends(get_session), user: User = Depends(get_current_active_user)): - """Return a new access & refresh token using on the old refresh token""" +async def refresh_access_token(db: Session = Depends(get_session), user: User = Depends(get_user_from_refresh_token)): + """Return a new access & refresh token using on the old refresh token + + Swagger note: This endpoint will not work on swagger because it uses the access token to try & refresh""" access_token, refresh_token = create_tokens(user) user_data: dict = user_model_to_schema(user).__dict__ diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index 805554ad9..3808a2a67 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -6,9 +6,10 @@ import settings import src.database.crud.projects as crud_projects -from src.app.exceptions.authentication import ExpiredCredentialsException, InvalidCredentialsException, \ - MissingPermissionsException -from src.app.logic.security import ALGORITHM +from src.app.exceptions.authentication import ( + ExpiredCredentialsException, InvalidCredentialsException, + MissingPermissionsException, WrongTokenTypeException) +from src.app.logic.security import ALGORITHM, TokenType 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.users import get_user_by_id @@ -24,18 +25,19 @@ def get_edition(edition_name: str, database: Session = Depends(get_session)) -> oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login/token") -async def get_current_active_user(db: Session = Depends(get_session), token: str = Depends(oauth2_scheme)) -> User: - """Check which user is making a request by decoding its token - This function is used as a dependency for other functions - """ - # TODO: check type of token. +async def _get_user_from_token(token_type: TokenType, db: Session, 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]) user_id: int | None = payload.get("sub") + type_in_token: int | None = payload.get("type") - if user_id is None: + if user_id is None or type_in_token is None: raise InvalidCredentialsException() + if type_in_token != token_type.value: + raise WrongTokenTypeException() + try: user = get_user_by_id(db, int(user_id)) except sqlalchemy.exc.NoResultFound as not_found: @@ -48,6 +50,20 @@ async def get_current_active_user(db: Session = Depends(get_session), token: str raise InvalidCredentialsException() from jwt_err +async def get_current_active_user(db: Session = 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: + """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 require_auth(user: User = Depends(get_current_active_user)) -> 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 diff --git a/backend/tests/utils/authorization/auth_client.py b/backend/tests/utils/authorization/auth_client.py index 65bb216a6..900c996b6 100644 --- a/backend/tests/utils/authorization/auth_client.py +++ b/backend/tests/utils/authorization/auth_client.py @@ -5,7 +5,7 @@ from sqlalchemy.orm import Session from starlette.testclient import TestClient -from src.app.logic.security import create_access_token +from src.app.logic.security import create_tokens from src.database.models import User, Edition @@ -53,10 +53,8 @@ def login(self, user: User): """Sign in as a user for all future requests""" self.user = user - access_token_expires = timedelta(hours=24*7) - access_token = create_access_token( - data={"sub": str(user.user_id)}, expires_delta=access_token_expires - ) + # Since an authclient is created for every test, the access_token will most likely not run out + access_token, _refresh_token = create_tokens(user) # Add auth headers into dict self.headers = {"Authorization": f"Bearer {access_token}"} From fc17264cb2ec03ed6f8ff68bdb50fd60e654f2cf Mon Sep 17 00:00:00 2001 From: FKD13 Date: Wed, 13 Apr 2022 12:04:39 +0200 Subject: [PATCH 379/536] update env.Example --- backend/.env.example | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index ec2a58e0b..2836e638d 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -7,8 +7,10 @@ DB_PORT=3306 # JWT key (needs to be changed for production) # Can be generated using "openssl rand -hex 32" SECRET_KEY=4d16a9cc83d74144322e893c879b5f639088c15dc1606b11226abbd7e97f5ee5 -# The JWT token should be valid for 24*7(=168) hours -ACCESS_TOKEN_EXPIRE_HOURS = 168 +# The ACCESS JWT token should be valid for ... +ACCESS_TOKEN_EXPIRE_MINUTES = 1 +# The REFRESH JWT token should be valid for ... +REFRESH_TOKEN_EXPIRE_MINUTES = 2 # Frontend FRONTEND_URL="http://localhost:3000" \ No newline at end of file From 1590145532d608b14822a54ab257ccdf553c6391 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Wed, 13 Apr 2022 15:31:51 +0200 Subject: [PATCH 380/536] fix import --- backend/src/database/crud/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 0952a5245..5f220bfc9 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -2,7 +2,7 @@ from src.database.crud.editions import get_edition_by_name from src.database.crud.util import paginate -from src.database.models import user_editions, User, Edition, CoachRequest +from src.database.models import user_editions, User, Edition, CoachRequest, AuthEmail, AuthGitHub, AuthGoogle from src.database.crud.editions import get_editions From e21a279757c903b7352bad51d27ba9dffdf3488d Mon Sep 17 00:00:00 2001 From: stijndcl Date: Wed, 13 Apr 2022 16:53:54 +0200 Subject: [PATCH 381/536] Fix typing --- backend/src/database/crud/users.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 5f220bfc9..206d4fdbe 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -12,11 +12,11 @@ def get_user_edition_names(db: Session, user: User) -> list[str]: source = user.editions if not user.admin else get_editions(db) editions = [] - # Name is non-nullable in the database, so it can never be None, + # 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, reverse=True): + for edition in sorted(source, key=lambda e: e.year or -1, reverse=True): if edition.name is not None: editions.append(edition.name) From 1937805e7ccfa13af0b638e71fb5e6a586fd8054 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Wed, 13 Apr 2022 23:14:25 +0200 Subject: [PATCH 382/536] Cleanup code + fix colors dropdowns --- .../components/AdminsComponents/AddAdmin.tsx | 32 +++++++- .../components/AdminsComponents/AdminList.tsx | 2 +- .../AdminsComponents/RemoveAdmin.tsx | 2 +- .../components/GeneralComponents/MenuItem.tsx | 15 ++++ .../src/components/GeneralComponents/index.ts | 1 + .../components/GeneralComponents/styles.ts | 21 +++++ .../UsersComponents/Coaches/Coaches.tsx | 21 ++--- .../Coaches/CoachesComponents/AddCoach.tsx | 36 ++++++++- .../Coaches/CoachesComponents/CoachList.tsx | 12 +-- .../CoachesComponents/CoachListItem.tsx | 10 ++- .../Coaches/CoachesComponents/RemoveCoach.tsx | 78 ++++++++++++------- .../UsersComponents/Coaches/styles.ts | 9 ++- .../UsersComponents/InviteUser/InviteUser.tsx | 29 +++++-- .../InviteUserComponents/ErrorDiv.tsx | 14 ---- .../InviteUserComponents/MessageDiv.tsx | 14 ---- .../{ButtonsDiv.tsx => SendInviteButton.tsx} | 14 ++-- .../InviteUser/InviteUserComponents/index.ts | 4 +- .../UsersComponents/InviteUser/styles.ts | 43 ++++------ .../UsersComponents/PendingRequests/index.ts | 2 - .../Requests.tsx} | 62 ++++++++------- .../RequestsComponents}/AcceptReject.tsx | 10 +-- .../RequestsComponents}/RequestList.tsx | 3 +- .../RequestsComponents}/RequestListItem.tsx | 6 +- .../RequestsComponents}/RequestsHeader.tsx | 2 +- .../RequestsComponents}/index.ts | 0 .../UsersComponents/Requests/index.ts | 2 + .../{PendingRequests => Requests}/styles.ts | 19 ++--- .../src/components/UsersComponents/index.ts | 2 +- frontend/src/components/index.ts | 1 + frontend/src/components/styles.ts | 11 +++ frontend/src/utils/api/users/admins.ts | 18 ++--- frontend/src/utils/api/users/coaches.ts | 30 +++---- frontend/src/utils/api/users/requests.ts | 22 ++---- frontend/src/utils/api/users/users.ts | 29 +++---- frontend/src/views/AdminsPage/AdminsPage.tsx | 7 +- frontend/src/views/UsersPage/UsersPage.tsx | 57 ++++++++++---- 36 files changed, 386 insertions(+), 254 deletions(-) create mode 100644 frontend/src/components/GeneralComponents/MenuItem.tsx create mode 100644 frontend/src/components/GeneralComponents/index.ts create mode 100644 frontend/src/components/GeneralComponents/styles.ts delete mode 100644 frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ErrorDiv.tsx delete mode 100644 frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/MessageDiv.tsx rename frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/{ButtonsDiv.tsx => SendInviteButton.tsx} (65%) delete mode 100644 frontend/src/components/UsersComponents/PendingRequests/index.ts rename frontend/src/components/UsersComponents/{PendingRequests/PendingRequests.tsx => Requests/Requests.tsx} (64%) rename frontend/src/components/UsersComponents/{PendingRequests/PendingRequestsComponents => Requests/RequestsComponents}/AcceptReject.tsx (82%) rename frontend/src/components/UsersComponents/{PendingRequests/PendingRequestsComponents => Requests/RequestsComponents}/RequestList.tsx (93%) rename frontend/src/components/UsersComponents/{PendingRequests/PendingRequestsComponents => Requests/RequestsComponents}/RequestListItem.tsx (79%) rename frontend/src/components/UsersComponents/{PendingRequests/PendingRequestsComponents => Requests/RequestsComponents}/RequestsHeader.tsx (95%) rename frontend/src/components/UsersComponents/{PendingRequests/PendingRequestsComponents => Requests/RequestsComponents}/index.ts (100%) create mode 100644 frontend/src/components/UsersComponents/Requests/index.ts rename frontend/src/components/UsersComponents/{PendingRequests => Requests}/styles.ts (83%) create mode 100644 frontend/src/components/styles.ts diff --git a/frontend/src/components/AdminsComponents/AddAdmin.tsx b/frontend/src/components/AdminsComponents/AddAdmin.tsx index 87080ae2d..3451e8da7 100644 --- a/frontend/src/components/AdminsComponents/AddAdmin.tsx +++ b/frontend/src/components/AdminsComponents/AddAdmin.tsx @@ -3,8 +3,10 @@ import React, { useState } from "react"; import { addAdmin } from "../../utils/api/users/admins"; import { AddAdminButton, ModalContentConfirm, Warning } from "./styles"; import { Button, Modal, Spinner } from "react-bootstrap"; -import { AsyncTypeahead } from "react-bootstrap-typeahead"; -import { Error } from "../UsersComponents/PendingRequests/styles"; +import { AsyncTypeahead, Menu } from "react-bootstrap-typeahead"; +import { Error } from "../UsersComponents/Requests/styles"; +import { StyledMenuItem } from "../GeneralComponents/styles"; +import UserMenuItem from "../GeneralComponents/MenuItem"; /** * Warning that the user will get all persmissions. @@ -134,7 +136,33 @@ export default function AddAdmin(props: { adminAdded: (user: User) => void }) { setSelected(selected[0] as User); setError(""); }} + renderMenu={(results, menuProps) => { + const { + newSelectionPrefix, + paginationText, + renderMenuItemChildren, + ...props + } = menuProps; + return ( + + {results.map((result, index) => { + const user = result as User; + return ( + + +
                                      +
                                      + ); + })} +
                                      + ); + }} /> + {selected?.auth.email} diff --git a/frontend/src/components/AdminsComponents/AdminList.tsx b/frontend/src/components/AdminsComponents/AdminList.tsx index 8fb33960c..b383c3de6 100644 --- a/frontend/src/components/AdminsComponents/AdminList.tsx +++ b/frontend/src/components/AdminsComponents/AdminList.tsx @@ -1,5 +1,5 @@ import { User } from "../../utils/api/users/users"; -import { SpinnerContainer } from "../UsersComponents/PendingRequests/styles"; +import { SpinnerContainer } from "../UsersComponents/Requests/styles"; import { Spinner } from "react-bootstrap"; import { AdminsTable } from "./styles"; import React from "react"; diff --git a/frontend/src/components/AdminsComponents/RemoveAdmin.tsx b/frontend/src/components/AdminsComponents/RemoveAdmin.tsx index ecedc3a1f..e62ccefba 100644 --- a/frontend/src/components/AdminsComponents/RemoveAdmin.tsx +++ b/frontend/src/components/AdminsComponents/RemoveAdmin.tsx @@ -3,7 +3,7 @@ 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/PendingRequests/styles"; +import { Error } from "../UsersComponents/Requests/styles"; /** * Button and popup to remove a user as admin (and as coach). diff --git a/frontend/src/components/GeneralComponents/MenuItem.tsx b/frontend/src/components/GeneralComponents/MenuItem.tsx new file mode 100644 index 000000000..c758c6859 --- /dev/null +++ b/frontend/src/components/GeneralComponents/MenuItem.tsx @@ -0,0 +1,15 @@ +import { User } from "../../utils/api/users/users"; +import { NameDiv, EmailDiv } from "./styles"; + +/** + * An item from a dropdown menu containing a user's name and email. + * @param props.user The user which is represented. + */ +export default function UserMenuItem(props: { user: User }) { + return ( +
                                      + {props.user.name} + {props.user.auth.email} +
                                      + ); +} diff --git a/frontend/src/components/GeneralComponents/index.ts b/frontend/src/components/GeneralComponents/index.ts new file mode 100644 index 000000000..cf0669a1a --- /dev/null +++ b/frontend/src/components/GeneralComponents/index.ts @@ -0,0 +1 @@ +export { default as MenuItem } from "./MenuItem"; diff --git a/frontend/src/components/GeneralComponents/styles.ts b/frontend/src/components/GeneralComponents/styles.ts new file mode 100644 index 000000000..1020162d1 --- /dev/null +++ b/frontend/src/components/GeneralComponents/styles.ts @@ -0,0 +1,21 @@ +import styled from "styled-components"; +import { MenuItem } from "react-bootstrap-typeahead"; + +export const StyledMenuItem = styled(MenuItem)` + color: white; + transition: 200ms ease-out; + + &:hover { + background-color: var(--osoc_blue); + color: var(--osoc_green); + transition: 200ms ease-in; + } +`; + +export const NameDiv = styled.div` + float: left; +`; + +export const EmailDiv = styled.div` + float: right; +`; diff --git a/frontend/src/components/UsersComponents/Coaches/Coaches.tsx b/frontend/src/components/UsersComponents/Coaches/Coaches.tsx index dcb7d50ea..8581f8e5a 100644 --- a/frontend/src/components/UsersComponents/Coaches/Coaches.tsx +++ b/frontend/src/components/UsersComponents/Coaches/Coaches.tsx @@ -1,27 +1,29 @@ import React from "react"; import { CoachesTitle, CoachesContainer } from "./styles"; import { User } from "../../../utils/api/users/users"; -import { Error, SearchInput, SpinnerContainer } from "../PendingRequests/styles"; +import { Error, SpinnerContainer } from "../Requests/styles"; import { CoachList, AddCoach } from "./CoachesComponents"; import { Spinner } from "react-bootstrap"; +import { SearchInput } from "../../styles"; /** * 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 need to be shown. - * @param props.coaches The list of all coaches of the current edition. - * @param props.refresh A function which will be called when a coach is added/removed. + * @param props.edition The edition of which coaches are shown. + * @param props.coaches The list of coaches which need to be shown. * @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 Data is not available yet. + * @param props.gettingData Waiting for data. * @param props.error An error message. * @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. + * @param props.removeCoach A function which will be called when a user is deleted as coach. */ export default function Coaches(props: { edition: string; coaches: User[]; - refresh: () => void; getMoreCoaches: (page: number) => void; searchCoaches: (word: string) => void; gotData: boolean; @@ -29,7 +31,8 @@ export default function Coaches(props: { error: string; moreCoachesAvailable: boolean; searchTerm: string; - coachAdded: (user: User) => void; + refreshCoaches: () => void; + removeCoach: (user: User) => void; }) { let table; if (props.coaches.length === 0) { @@ -51,7 +54,7 @@ export default function Coaches(props: { loading={props.gettingData} edition={props.edition} gotData={props.gotData} - refresh={props.refresh} + removeCoach={props.removeCoach} getMoreCoaches={props.getMoreCoaches} moreCoachesAvailable={props.moreCoachesAvailable} /> @@ -65,7 +68,7 @@ export default function Coaches(props: { value={props.searchTerm} onChange={e => props.searchCoaches(e.target.value)} /> - + {table} ); diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx index aafbf59fd..25962feff 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx @@ -2,9 +2,11 @@ import { getUsersExcludeEdition, User } from "../../../../utils/api/users/users" import React, { useState } from "react"; import { addCoachToEdition } from "../../../../utils/api/users/coaches"; import { Button, Modal, Spinner } from "react-bootstrap"; -import { AsyncTypeahead } from "react-bootstrap-typeahead"; -import { Error } from "../../PendingRequests/styles"; +import { Error } from "../../Requests/styles"; import { AddAdminButton, ModalContentConfirm } from "../../../AdminsComponents/styles"; +import { AsyncTypeahead, Menu } from "react-bootstrap-typeahead"; +import UserMenuItem from "../../../GeneralComponents/MenuItem"; +import { StyledMenuItem } from "../../../GeneralComponents/styles"; /** * A button and popup to add a new coach to the given edition. @@ -12,7 +14,7 @@ import { AddAdminButton, ModalContentConfirm } from "../../../AdminsComponents/s * @param props.edition The edition to which users need to be added. * @param props.coachAdded A function which will be called when a user is added as coach. */ -export default function AddCoach(props: { edition: string; coachAdded: (user: User) => void }) { +export default function AddCoach(props: { edition: string; refreshCoaches: () => void }) { const [show, setShow] = useState(false); const [selected, setSelected] = useState(undefined); const [loading, setLoading] = useState(false); @@ -71,7 +73,7 @@ export default function AddCoach(props: { edition: string; coachAdded: (user: Us } setLoading(false); if (success) { - props.coachAdded(user); + props.refreshCoaches(); handleClose(); } } @@ -120,7 +122,33 @@ export default function AddCoach(props: { edition: string; coachAdded: (user: Us setSelected(selected[0] as User); setError(""); }} + renderMenu={(results, menuProps) => { + const { + newSelectionPrefix, + paginationText, + renderMenuItemChildren, + ...props + } = menuProps; + return ( + + {results.map((result, index) => { + const user = result as User; + return ( + + +
                                      +
                                      + ); + })} +
                                      + ); + }} /> + {selected?.auth.email} {addButton} diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx index 763dd1b06..ba28fb538 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx @@ -1,5 +1,5 @@ import { User } from "../../../../utils/api/users/users"; -import { SpinnerContainer } from "../../PendingRequests/styles"; +import { SpinnerContainer } from "../../Requests/styles"; import { Spinner } from "react-bootstrap"; import { CoachesTable, ListDiv, RemoveTh } from "../styles"; import React from "react"; @@ -12,16 +12,16 @@ import { CoachListItem } from "./index"; * @param props.loading Data is not available yet. * @param props.edition The edition. * @param props.gotData All data is received. - * @param props.refresh A function which will be called when a coach is removed. + * @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 + * @param props.moreCoachesAvailable More unfetched coaches available. */ export default function CoachList(props: { coaches: User[]; loading: boolean; edition: string; gotData: boolean; - refresh: () => void; + removeCoach: (coach: User) => void; getMoreCoaches: (page: number) => void; moreCoachesAvailable: boolean; }) { @@ -37,7 +37,7 @@ export default function CoachList(props: { } useWindow={false} - initialLoad={false} + initialLoad={true} > @@ -53,7 +53,7 @@ export default function CoachList(props: { key={coach.userId} coach={coach} edition={props.edition} - refresh={props.refresh} + removeCoach={props.removeCoach} /> ))} diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx index b22c08809..c1ca8015b 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx @@ -8,19 +8,23 @@ import { RemoveTd } from "../styles"; * This includes a button te remove the coach. * @param props.coach The coach which is represented. * @param props.edition The edition whereof the user is coach. - * @param props.refresh A function which will be called when the coach is removed. + * @param props.removeCoach A function which will be called when the coach is removed. */ export default function CoachListItem(props: { coach: User; edition: string; - refresh: () => void; + removeCoach: (coach: User) => void; }) { return ( {props.coach.name} {props.coach.auth.email} - + props.removeCoach(props.coach)} + /> ); diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx index 67fd58a9c..6c14383ce 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx @@ -4,30 +4,42 @@ import { removeCoachFromAllEditions, removeCoachFromEdition, } from "../../../../utils/api/users/coaches"; -import { Button, Modal } from "react-bootstrap"; +import { Button, Modal, Spinner } from "react-bootstrap"; import { ModalContent } from "../styles"; -import { Error } from "../../PendingRequests/styles"; +import { Error } from "../../Requests/styles"; /** - * A button and popup to remove a user as coach from the given edition or all editions. + * A button (part of [[CoachListItem]]) and popup to remove a user as coach from the given edition or all editions. * The popup gives the choice between removing the user as coach from this edition or all editions. * @param props.coach The coach which can be removed. * @param props.edition The edition of which the coach can be removed. - * @param props.refresh A function which will be called when a user is removed as coach. + * @param props.removeCoach A function which will be called when a user is removed as coach. */ -export default function RemoveCoach(props: { coach: User; edition: string; refresh: () => void }) { +export default function RemoveCoach(props: { + coach: User; + edition: string; + 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(""); }; + /** + * Remove a coach from the current edition or all editions. + * @param userId The id of the coach + * @param allEditions Boolean whether the coach should be removed from all editions he's coach from. + */ async function removeCoach(userId: number, allEditions: boolean) { + setLoading(true); + let removed = false; try { - let removed; if (allEditions) { removed = await removeCoachFromAllEditions(userId); } else { @@ -35,16 +47,46 @@ export default function RemoveCoach(props: { coach: User; edition: string; refre } if (removed) { - props.refresh(); - handleClose(); + 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); } } + let buttons; + if (loading) { + buttons = ; + } else { + buttons = ( +
                                      + + + +
                                      + ); + } + return ( <> - - + {buttons} {error}
                                      diff --git a/frontend/src/components/UsersComponents/Coaches/styles.ts b/frontend/src/components/UsersComponents/Coaches/styles.ts index d5eecceb8..f06d718e0 100644 --- a/frontend/src/components/UsersComponents/Coaches/styles.ts +++ b/frontend/src/components/UsersComponents/Coaches/styles.ts @@ -2,8 +2,9 @@ import styled from "styled-components"; import { Table } from "react-bootstrap"; export const CoachesContainer = styled.div` - width: 50%; - min-width: 600px; + min-width: 450px; + width: 80%; + max-width: 700px; height: 500px; margin: 10px auto auto; `; @@ -15,7 +16,9 @@ export const CoachesTitle = styled.div` font-size: 25px; `; -export const CoachesTable = styled(Table)``; +export const CoachesTable = styled(Table)` + // TODO: make all tables in site uniform +`; export const ModalContent = styled.div` border: 3px solid var(--osoc_red); diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx index bbdc9c87e..68b4a09fd 100644 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUser.tsx @@ -1,13 +1,14 @@ import React, { useState } from "react"; import { getInviteLink } from "../../../utils/api/users/users"; import "./InviteUser.css"; -import { InviteInput, InviteContainer } from "./styles"; -import { ButtonsDiv, ErrorDiv, MessageDiv } from "./InviteUserComponents"; +import { InviteInput, InviteContainer, Error, MessageDiv } from "./styles"; +import { ButtonsDiv } from "./InviteUserComponents"; /** * A component to invite a user as coach to a given edition. - * Contains an input field for the email address of the new user - * and a button to get a mailto link which contains the invite link or just the invite link. + * Contains an input field for the email address of the new user. + * and a button to get a mailto link which contains the invite link, + * or to copy the invite link to clipboard. * @param props.edition The edition whereto the person will be invited. */ export default function InviteUser(props: { edition: string }) { @@ -17,6 +18,11 @@ export default function InviteUser(props: { edition: string }) { const [loading, setLoading] = useState(false); // The invite link is being created const [message, setMessage] = useState(""); // A message to confirm link created + /** + * Change the content of the email field. + * Remove error and message (user is probably still typing). + * @param email The string set in the input filed. + */ const changeEmail = function (email: string) { setEmail(email); setValid(true); @@ -24,6 +30,13 @@ export default function InviteUser(props: { edition: string }) { setMessage(""); }; + /** + * Check if the form of the email is valid. + * Send a request to backend to get the invite link. + * Depending on the copyInvite parameter, the recieved invite link will be put in an mailto, + * or copied to the user's clipboard. + * @param copyInvite Boolean to indicate wether the invite should be copied to clipboard or a mailto should be created. + */ const sendInvite = async (copyInvite: boolean) => { if (/[^@\s]+@[^@\s]+\.[^@\s]+/.test(email)) { setLoading(true); @@ -34,7 +47,7 @@ export default function InviteUser(props: { edition: string }) { setMessage("Copied invite link for " + email); } else { window.open(response.mailTo); - setMessage("Created mail for " + email); + setMessage("Created email for " + email); } setLoading(false); setEmail(""); @@ -60,8 +73,10 @@ export default function InviteUser(props: { edition: string }) { /> - - + + {message} + {errorMessage} +
                                      ); } diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ErrorDiv.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ErrorDiv.tsx deleted file mode 100644 index 149413d6d..000000000 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ErrorDiv.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Error } from "../styles"; -import React from "react"; - -/** - * A component which shows an error if there is one. - * @param props.errorMessage The possible message. - */ -export default function ErrorDiv(props: { errorMessage: string }) { - let errorDiv = null; - if (props.errorMessage) { - errorDiv = {props.errorMessage}; - } - return errorDiv; -} diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/MessageDiv.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/MessageDiv.tsx deleted file mode 100644 index 46c938c88..000000000 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/MessageDiv.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Message } from "../styles"; -import React from "react"; - -/** - * A component which shows a message if there is one. - * @param props.message The possible message. - */ -export default function MessageDiv(props: { message: string }) { - let messageDiv = null; - if (props.message) { - messageDiv = {props.message}; - } - return messageDiv; -} diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ButtonsDiv.tsx b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/SendInviteButton.tsx similarity index 65% rename from frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ButtonsDiv.tsx rename to frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/SendInviteButton.tsx index d7cf21d70..97a8f3030 100644 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/ButtonsDiv.tsx +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/SendInviteButton.tsx @@ -1,18 +1,18 @@ -import { InviteButton, Loader } from "../styles"; +import { DropdownField, InviteButton } from "../styles"; import React from "react"; -import { Button, ButtonGroup, Dropdown } from "react-bootstrap"; +import { Button, ButtonGroup, Dropdown, Spinner } from "react-bootstrap"; /** * A component to choice between sending an invite or copying it to clipboard. - * @param props.loading Invite is being created. + * @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 ButtonsDiv(props: { +export default function SendInviteButton(props: { loading: boolean; sendInvite: (copy: boolean) => void; }) { if (props.loading) { - return ; + return ; } return ( @@ -24,9 +24,9 @@ export default function ButtonsDiv(props: { - props.sendInvite(true)}> + props.sendInvite(true)}> Copy invite link - + diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/index.ts b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/index.ts index df676306a..c99ca8d78 100644 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/index.ts +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUserComponents/index.ts @@ -1,3 +1 @@ -export { default as ButtonsDiv } from "./ButtonsDiv"; -export { default as ErrorDiv } from "./ErrorDiv"; -export { default as MessageDiv } from "./MessageDiv"; +export { default as ButtonsDiv } from "./SendInviteButton"; diff --git a/frontend/src/components/UsersComponents/InviteUser/styles.ts b/frontend/src/components/UsersComponents/InviteUser/styles.ts index 5b30a046f..d264261f6 100644 --- a/frontend/src/components/UsersComponents/InviteUser/styles.ts +++ b/frontend/src/components/UsersComponents/InviteUser/styles.ts @@ -1,4 +1,5 @@ -import styled, { keyframes } from "styled-components"; +import styled from "styled-components"; +import { Dropdown } from "react-bootstrap"; export const InviteContainer = styled.div` clear: both; @@ -20,37 +21,27 @@ export const InviteInput = styled.input.attrs({ float: left; `; -const rotate = keyframes` - from { - transform: rotate(0deg); - } - - to { - transform: rotate(360deg); - } -`; - -export const Loader = styled.div` - border: 8px solid var(--osoc_green); - border-top: 8px solid var(--osoc_blue); - border-radius: 50%; - width: 35px; - height: 35px; - animation: ${rotate} 2s linear infinite; - margin-left: 37px; - margin-top: 10px; - float: left; +export const MessageDiv = styled.div` + margin-left: 10px; + margin-top: 5px; + height: 15px; `; export const Error = styled.div` - margin-left: 10px; color: var(--osoc_red); `; -export const Message = styled.div` - margin-left: 10px; -`; - export const InviteButton = styled.div` padding-top: 10px; `; + +export const DropdownField = styled(Dropdown.Item)` + color: white; + transition: 200ms ease-out; + + &:hover { + background-color: var(--osoc_blue); + color: var(--osoc_green); + transition: 200ms ease-in; + } +`; diff --git a/frontend/src/components/UsersComponents/PendingRequests/index.ts b/frontend/src/components/UsersComponents/PendingRequests/index.ts deleted file mode 100644 index ac5bdf1ca..000000000 --- a/frontend/src/components/UsersComponents/PendingRequests/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as PendingRequests } from "./PendingRequests"; -export * as PendingRequestsComponents from "./PendingRequestsComponents"; diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx b/frontend/src/components/UsersComponents/Requests/Requests.tsx similarity index 64% rename from frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx rename to frontend/src/components/UsersComponents/Requests/Requests.tsx index 1ac708d3c..59f201a3b 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequests.tsx +++ b/frontend/src/components/UsersComponents/Requests/Requests.tsx @@ -1,46 +1,50 @@ import React, { useEffect, useState } from "react"; import Collapsible from "react-collapsible"; -import { - PendingRequestsContainer, - Error, - SearchInput, - SpinnerContainer, - RequestListContainer, -} from "./styles"; +import { RequestsContainer, Error, SpinnerContainer, RequestListContainer } from "./styles"; import { getRequests, Request } from "../../../utils/api/users/requests"; -import { RequestList, RequestsHeader } from "./PendingRequestsComponents"; -import { User } from "../../../utils/api/users/users"; +import { RequestList, RequestsHeader } from "./RequestsComponents"; import { Spinner } from "react-bootstrap"; +import { SearchInput } from "../../styles"; /** * A collapsible component which contains all coach requests for a given edition. * Every request can be accepted or rejected. * @param props.edition The edition. - * @param props.coachAdded A funciton to call when a new coach is added + * @param props.refreshCoaches A function which will be called when a new coach is added */ -export default function PendingRequests(props: { - edition: string; - coachAdded: (user: User) => void; -}) { +export default function Requests(props: { edition: string; refreshCoaches: () => void }) { const [requests, setRequests] = useState([]); // All requests after filter const [gettingRequests, setGettingRequests] = useState(false); // Waiting for data - const [searchTerm, setSearchTerm] = useState(""); // The word set in filter + const [searchTerm, setSearchTerm] = useState(""); // The word set in the filter const [gotData, setGotData] = useState(false); // Received data const [open, setOpen] = useState(false); // Collapsible is open const [error, setError] = useState(""); // Error message - const [moreRequestsAvailable, setMoreRequestsAvailable] = useState(true); + const [moreRequestsAvailable, setMoreRequestsAvailable] = useState(true); // Endpoint has more requests available - function removeRequest(coachAdded: boolean, request: Request) { + /** + * Remove a request from the list of requests (Request is accepter or rejected). + * When the request was accepted, the refreshCoaches will be called. + * @param accepted Boolean to say if a coach was accepted. + * @param request The request which was accepter or rejected. + */ + function removeRequest(accepted: boolean, request: Request) { setRequests( requests.filter(object => { return object !== request; }) ); - if (coachAdded) { - props.coachAdded(request.user); + 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. + */ async function getData(page: number, filter: string | undefined = undefined) { if (filter === undefined) { filter = searchTerm; @@ -49,7 +53,7 @@ export default function PendingRequests(props: { setError(""); try { const response = await getRequests(props.edition, filter, page); - if (response.requests.length !== 25) { + if (response.requests.length === 0) { setMoreRequestsAvailable(false); } if (page === 0) { @@ -70,16 +74,20 @@ export default function PendingRequests(props: { if (!gotData && !gettingRequests && !error) { getData(0); } - }, [gotData, gettingRequests, error, getData]); + }); - const searchRequests = (searchTerm: string) => { - setGettingRequests(true); + /** + * 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); - }; + } let list; if (requests.length === 0) { @@ -106,15 +114,15 @@ export default function PendingRequests(props: { } return ( - + } onOpening={() => setOpen(true)} onClosing={() => setOpen(false)} > - searchRequests(e.target.value)} /> + filterRequests(e.target.value)} /> {list} - + ); } diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/AcceptReject.tsx b/frontend/src/components/UsersComponents/Requests/RequestsComponents/AcceptReject.tsx similarity index 82% rename from frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/AcceptReject.tsx rename to frontend/src/components/UsersComponents/Requests/RequestsComponents/AcceptReject.tsx index 74f0590a4..5284ecf26 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/AcceptReject.tsx +++ b/frontend/src/components/UsersComponents/Requests/RequestsComponents/AcceptReject.tsx @@ -5,12 +5,12 @@ import { Spinner } from "react-bootstrap"; /** * Component consisting of two buttons to accept or reject a coach request. - * @param props.requestId The id of the request. - * @param props.refresh A function which will be called when a request is accepted/rejected. + * @param props.request The request which can be accepted/rejected. + * @param props.removeRequest A function which will be called when a request is accepted/rejected. */ export default function AcceptReject(props: { request: Request; - refresh: (coachAdded: boolean, request: Request) => void; + removeRequest: (coachAdded: boolean, request: Request) => void; }) { const [loading, setLoading] = useState(false); const [error, setError] = useState(""); @@ -28,7 +28,7 @@ export default function AcceptReject(props: { } setLoading(false); if (success) { - props.refresh(true, props.request); + props.removeRequest(true, props.request); } } @@ -45,7 +45,7 @@ export default function AcceptReject(props: { } setLoading(false); if (success) { - props.refresh(false, props.request); + props.removeRequest(false, props.request); } } diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx b/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestList.tsx similarity index 93% rename from frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx rename to frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestList.tsx index ddec6c3ce..f85315cb9 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestList.tsx +++ b/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestList.tsx @@ -11,6 +11,7 @@ import { ListDiv } from "../../Coaches/styles"; * @param props.requests A list of requests which need to be shown. * @param props.removeRequest A function which will be called when a request is accepted/rejected. * @param props.moreRequestsAvailable Boolean to indicate whether more requests can be fetched + * @param props.getMoreRequests A function which will be called when more requests need to be loaded. */ export default function RequestList(props: { requests: Request[]; @@ -30,7 +31,7 @@ export default function RequestList(props: { } useWindow={false} - initialLoad={false} + initialLoad={true} > diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestListItem.tsx b/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestListItem.tsx similarity index 79% rename from frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestListItem.tsx rename to frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestListItem.tsx index 1a3a8c419..3d6f3c94b 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestListItem.tsx +++ b/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestListItem.tsx @@ -5,9 +5,9 @@ import { AcceptRejectTd } from "../styles"; /** * An item from [[RequestList]] which represents one request. - * This includes two buttons to accept and reject the request. + * This includes two buttons to accept or reject the request. * @param props.request The request which is represented. - * @param props.removeRequest A function which will be called when a request is accepted/rejected. + * @param props.removeRequest A function which will be called when the request is accepted/rejected. */ export default function RequestListItem(props: { request: Request; @@ -18,7 +18,7 @@ export default function RequestListItem(props: { {props.request.user.name} {props.request.user.auth.email} - + ); diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestsHeader.tsx b/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestsHeader.tsx similarity index 95% rename from frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestsHeader.tsx rename to frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestsHeader.tsx index df2c0ba42..1bca431f8 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/RequestsHeader.tsx +++ b/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestsHeader.tsx @@ -14,7 +14,7 @@ function Arrow(props: { open: boolean }) { } /** - * The header of [[PendingRequests]]. + * The header of [[Requests]]. * @param props.open Boolean to indicate if the collapsible is open. */ export default function RequestsHeader(props: { open: boolean }) { diff --git a/frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/index.ts b/frontend/src/components/UsersComponents/Requests/RequestsComponents/index.ts similarity index 100% rename from frontend/src/components/UsersComponents/PendingRequests/PendingRequestsComponents/index.ts rename to frontend/src/components/UsersComponents/Requests/RequestsComponents/index.ts diff --git a/frontend/src/components/UsersComponents/Requests/index.ts b/frontend/src/components/UsersComponents/Requests/index.ts new file mode 100644 index 000000000..5c11e2e33 --- /dev/null +++ b/frontend/src/components/UsersComponents/Requests/index.ts @@ -0,0 +1,2 @@ +export { default as PendingRequests } from "./Requests"; +export * as RequestsComponents from "./RequestsComponents"; diff --git a/frontend/src/components/UsersComponents/PendingRequests/styles.ts b/frontend/src/components/UsersComponents/Requests/styles.ts similarity index 83% rename from frontend/src/components/UsersComponents/PendingRequests/styles.ts rename to frontend/src/components/UsersComponents/Requests/styles.ts index 775e305b4..a0bf15341 100644 --- a/frontend/src/components/UsersComponents/PendingRequests/styles.ts +++ b/frontend/src/components/UsersComponents/Requests/styles.ts @@ -27,21 +27,14 @@ export const ClosedArrow = styled(BiDownArrow)` offset: 0 30px; `; -export const SearchInput = styled.input.attrs({ - placeholder: "Search", -})` - margin: 3px; - width: 150px; - font-size: 15px; - border-radius: 5px; - border-width: 0; +export const RequestsTable = styled(Table)` + // TODO: make all tables in site uniform `; -export const RequestsTable = styled(Table)``; - -export const PendingRequestsContainer = styled.div` - width: 50%; - min-width: 600px; +export const RequestsContainer = styled.div` + min-width: 450px; + width: 80%; + max-width: 700px; margin: 10px auto auto; `; diff --git a/frontend/src/components/UsersComponents/index.ts b/frontend/src/components/UsersComponents/index.ts index bf6dda9d5..7390166ef 100644 --- a/frontend/src/components/UsersComponents/index.ts +++ b/frontend/src/components/UsersComponents/index.ts @@ -1,3 +1,3 @@ export * as Coaches from "./Coaches"; export * as InviteUser from "./InviteUser"; -export * as PendingRequests from "./PendingRequests"; +export * as PendingRequests from "./Requests"; diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index a535a4923..6aef377a1 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -7,3 +7,4 @@ export { default as PrivateRoute } from "./PrivateRoute"; export * as RegisterComponents from "./RegisterComponents"; export * as UsersComponents from "./UsersComponents"; export * as AdminsComponents from "./AdminsComponents"; +export * as GeneralComponents from "./GeneralComponents"; diff --git a/frontend/src/components/styles.ts b/frontend/src/components/styles.ts new file mode 100644 index 000000000..f7091348b --- /dev/null +++ b/frontend/src/components/styles.ts @@ -0,0 +1,11 @@ +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; +`; diff --git a/frontend/src/utils/api/users/admins.ts b/frontend/src/utils/api/users/admins.ts index e0b520e69..f94c0deef 100644 --- a/frontend/src/utils/api/users/admins.ts +++ b/frontend/src/utils/api/users/admins.ts @@ -2,22 +2,22 @@ import { UsersList } from "./users"; import { axiosInstance } from "../api"; /** - * Get all admins + * Get a page from all admins. + * @param page The requested page. + * @param name A string which every username should contain (can be empty). */ export async function getAdmins(page: number, name: string): Promise { if (name) { const response = await axiosInstance.get(`/users?page=${page}&admin=true&name=${name}`); - // console.log(`|page: ${page} Search:${name} Found: ${response.data.users.length}`); return response.data as UsersList; } const response = await axiosInstance.get(`/users?page=${page}&admin=true`); - // console.log(`|page: ${page} Search:${name} Found: ${response.data.users.length}`); return response.data as UsersList; } /** - * Make the given user admin - * @param {number} userId The id of the user + * 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 }); @@ -25,8 +25,8 @@ export async function addAdmin(userId: number): Promise { } /** - * Remove the given user as admin - * @param {number} userId The id of the user + * Remove the given user as admin. + * @param {number} userId The id of the user. */ export async function removeAdmin(userId: number) { const response = await axiosInstance.patch(`/users/${userId}`, { admin: false }); @@ -34,8 +34,8 @@ export async function removeAdmin(userId: number) { } /** - * Remove the given user as admin and remove him as coach for every edition - * @param {number} userId The id of the user + * Remove the given user as admin and remove him as coach for every edition. + * @param {number} userId The id of the user. */ export async function removeAdminAndCoach(userId: number) { const response1 = 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 d675b5056..9762564fb 100644 --- a/frontend/src/utils/api/users/coaches.ts +++ b/frontend/src/utils/api/users/coaches.ts @@ -2,30 +2,26 @@ import { UsersList } from "./users"; import { axiosInstance } from "../api"; /** - * Get all coaches from the given edition - * @param edition The edition name - * @param name The username to filter - * @param page + * 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. */ export async function getCoaches(edition: string, name: string, page: number): Promise { - // eslint-disable-next-line promise/param-names - // await new Promise(r => setTimeout(r, 2000)); if (name) { const response = await axiosInstance.get( `/users/?edition=${edition}&page=${page}&name=${name}` ); - // console.log(`|page: ${page} Search:${name} Found: ${response.data.users.length}`); return response.data as UsersList; } const response = await axiosInstance.get(`/users/?edition=${edition}&page=${page}`); - // console.log(`|page: ${page} Search:${name} Found: ${response.data.users.length}`); return response.data as UsersList; } /** - * Remove a user as coach from the given edition - * @param {number} userId The user's id - * @param {string} edition The edition's name + * Remove a user as coach from the given edition. + * @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}`); @@ -33,8 +29,8 @@ export async function removeCoachFromEdition(userId: number, edition: string): P } /** - * Remove a user as coach from all editions - * @param {number} userId The user's id + * 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`); @@ -42,13 +38,11 @@ export async function removeCoachFromAllEditions(userId: number): Promise { - // eslint-disable-next-line promise/param-names - // await new Promise(r => setTimeout(r, 2000)); const response = await axiosInstance.post(`/users/${userId}/editions/${edition}`); return response.status === 204; } diff --git a/frontend/src/utils/api/users/requests.ts b/frontend/src/utils/api/users/requests.ts index 4b59be275..b40888652 100644 --- a/frontend/src/utils/api/users/requests.ts +++ b/frontend/src/utils/api/users/requests.ts @@ -2,7 +2,7 @@ import { User } from "./users"; import { axiosInstance } from "../api"; /** - * Interface for a request + * Interface of a request */ export interface Request { requestId: number; @@ -10,14 +10,14 @@ export interface Request { } /** - * Interface for a list of requests + * Interface of a list of requests */ export interface GetRequestsResponse { requests: Request[]; } /** - * Get all pending requests of a given edition. + * Get a page from all pending requests of a given edition. * @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. @@ -27,38 +27,30 @@ export async function getRequests( name: string, page: number ): Promise { - // eslint-disable-next-line promise/param-names - // await new Promise(r => setTimeout(r, 2000)); if (name) { const response = await axiosInstance.get( `/users/requests?edition=${edition}&page=${page}&user=${name}` ); - // console.log(`|page: ${page} Search:${name} Found: ${response.data.requests.length}`); return response.data as GetRequestsResponse; } const response = await axiosInstance.get(`/users/requests?edition=${edition}&page=${page}`); - // console.log(`|page: ${page} Search:${name} Found: ${response.data.requests.length}`); return response.data as GetRequestsResponse; } /** - * Accept a coach request - * @param {number} requestId The id of the request + * Accept a coach request. + * @param {number} requestId The id of the request. */ export async function acceptRequest(requestId: number): Promise { - // eslint-disable-next-line promise/param-names - // await new Promise(r => setTimeout(r, 2000)); const response = await axiosInstance.post(`/users/requests/${requestId}/accept`); return response.status === 204; } /** - * Reject a coach request - * @param {number} requestId The id of the request + * Reject a coach request. + * @param {number} requestId The id of the request.s */ export async function rejectRequest(requestId: number): Promise { - // eslint-disable-next-line promise/param-names - // await new Promise(r => setTimeout(r, 2000)); const response = await axiosInstance.post(`/users/requests/${requestId}/reject`); return response.status === 204; } diff --git a/frontend/src/utils/api/users/users.ts b/frontend/src/utils/api/users/users.ts index 219d8ed45..1c32cf8d5 100644 --- a/frontend/src/utils/api/users/users.ts +++ b/frontend/src/utils/api/users/users.ts @@ -1,7 +1,7 @@ import { axiosInstance } from "../api"; /** - * Interface for a user + * Interface of a user. */ export interface User { userId: number; @@ -13,12 +13,15 @@ export interface User { }; } +/** + * Interface of a list of users. + */ export interface UsersList { users: User[]; } /** - * Interface for a mailto link + * Interface of a mailto link. */ export interface MailTo { mailTo: string; @@ -26,7 +29,9 @@ export interface MailTo { } /** - * Get invite link for given email and edition + * Get an invite link for the given edition and email address. + * @param edition The edition whereto the email address will be invited. + * @param email The email address whereto the invite will be sent. */ export async function getInviteLink(edition: string, email: string): Promise { const response = await axiosInstance.post(`/editions/${edition}/invites/`, { email: email }); @@ -34,40 +39,36 @@ export async function getInviteLink(edition: string, email: string): Promise { - // eslint-disable-next-line promise/param-names - // await new Promise(r => setTimeout(r, 2000)); if (name) { - console.log(`/users/?page=${page}&exclude_edition=${edition}&name=${name}`); const response = await axiosInstance.get( `/users/?page=${page}&exclude_edition=${edition}&name=${name}` ); - console.log(`|page: ${page} Search:${name} Found: ${response.data.users.length}`); return response.data as UsersList; } const response = await axiosInstance.get(`/users/?exclude_edition=${edition}&page=${page}`); - console.log(`|page: ${page} Search:${name} Found: ${response.data.users.length}`); return response.data as UsersList; } /** - * Get all users who are not admin + * Get a page of all users who are not an admin. + * @param name The name which every user's name must contain (can be empty). + * @param page The requested page. */ export async function getUsersNonAdmin(name: string, page: number): Promise { - // eslint-disable-next-line promise/param-names - // await new Promise(r => setTimeout(r, 2000)); if (name) { const response = await axiosInstance.get(`/users/?page=${page}&admin=false&name=${name}`); - console.log(`|page: ${page} Found: ${response.data.users.length}`); return response.data as UsersList; } const response = await axiosInstance.get(`/users/?admin=false&page=${page}`); - console.log(`|page: ${page} Found: ${response.data.users.length}`); return response.data as UsersList; } diff --git a/frontend/src/views/AdminsPage/AdminsPage.tsx b/frontend/src/views/AdminsPage/AdminsPage.tsx index be69dbf79..29ec9b2be 100644 --- a/frontend/src/views/AdminsPage/AdminsPage.tsx +++ b/frontend/src/views/AdminsPage/AdminsPage.tsx @@ -1,14 +1,11 @@ import React, { useEffect, useState } from "react"; import { AdminsContainer } from "./styles"; import { getAdmins } from "../../utils/api/users/admins"; -import { - Error, - SearchInput, - SpinnerContainer, -} from "../../components/UsersComponents/PendingRequests/styles"; +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"; export default function AdminsPage() { const [admins, setAdmins] = useState([]); diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index f2e5255f0..d1ddfe44f 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -3,7 +3,7 @@ import { useParams, useNavigate } from "react-router-dom"; import { UsersPageDiv, AdminsButton, UsersHeader } from "./styles"; import { Coaches } from "../../components/UsersComponents/Coaches"; import { InviteUser } from "../../components/UsersComponents/InviteUser"; -import { PendingRequests } from "../../components/UsersComponents/PendingRequests"; +import { PendingRequests } from "../../components/UsersComponents/Requests"; import { User } from "../../utils/api/users/users"; import { getCoaches } from "../../utils/api/users/coaches"; @@ -11,16 +11,24 @@ import { getCoaches } from "../../utils/api/users/coaches"; * Page for admins to manage coach and admin settings. */ function UsersPage() { - const [coaches, setCoaches] = useState([]); // All coaches from the edition - const [gettingData, setGettingData] = useState(false); // Waiting for data + // Note: The coaches are not in the coaches component because accepting a request needs to refresh the coaches list. + 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 [moreCoachesAvailable, setMoreCoachesAvailable] = useState(true); - const [searchTerm, setSearchTerm] = useState(""); // The word set in filter + const [moreCoachesAvailable, setMoreCoachesAvailable] = useState(true); // Endpoint has more coaches available + const [searchTerm, setSearchTerm] = useState(""); // The word set in filter for coachlist const params = useParams(); 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. + */ async function getCoachesData(page: number, filter: string | undefined = undefined) { if (filter === undefined) { filter = searchTerm; @@ -29,7 +37,7 @@ function UsersPage() { setError(""); try { const coachResponse = await getCoaches(params.editionId as string, filter, page); - if (coachResponse.users.length !== 25) { + if (coachResponse.users.length === 0) { setMoreCoachesAvailable(false); } if (page === 0) { @@ -52,6 +60,11 @@ function UsersPage() { } }); + /** + * 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); @@ -60,13 +73,31 @@ function UsersPage() { getCoachesData(0, searchTerm); } - function coachAdded(coach: User) { - if (coach.name.includes(searchTerm)) { - setCoaches([coach].concat(coaches)); - } + /** + * Reset the list of coaches and get the first page. + * Used when a new coach is added. + */ + function refreshCoaches() { + setGotData(false); + setCoaches([]); + setMoreCoachesAvailable(true); + getCoachesData(0); + } + + /** + * Remove a coach from the list of coaches. + * @param coach The coach which needs to be deleted. + */ + function removeCoach(coach: User) { + setCoaches( + coaches.filter(object => { + return object !== coach; + }) + ); } if (params.editionId === undefined) { + // If this happens, User should be redirected to error page return
                                      Error
                                      ; } else { return ( @@ -78,11 +109,10 @@ function UsersPage() { navigate("/admins")}>Edit Admins
                                      - + getCoachesData(0)} gotData={gotData} gettingData={gettingData} error={error} @@ -90,7 +120,8 @@ function UsersPage() { searchCoaches={filterCoachesData} moreCoachesAvailable={moreCoachesAvailable} searchTerm={searchTerm} - coachAdded={coachAdded} + refreshCoaches={refreshCoaches} + removeCoach={removeCoach} /> ); From 5199e719d3b0fcad84cd45dd54568b7b677cf224 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Wed, 13 Apr 2022 23:54:15 +0200 Subject: [PATCH 383/536] Add Auth type next to email everywhere --- .../components/AdminsComponents/AddAdmin.tsx | 3 ++- .../AdminsComponents/AdminListItem.tsx | 5 ++++- .../GeneralComponents/AuthTypeIcon.tsx | 19 +++++++++++++++++ .../GeneralComponents/EmailAndAuth.tsx | 21 +++++++++++++++++++ .../components/GeneralComponents/MenuItem.tsx | 7 +++++-- .../src/components/GeneralComponents/index.ts | 2 ++ .../components/GeneralComponents/styles.ts | 9 ++++++++ .../Coaches/CoachesComponents/AddCoach.tsx | 3 ++- .../CoachesComponents/CoachListItem.tsx | 5 ++++- .../RequestsComponents/RequestListItem.tsx | 5 ++++- frontend/src/utils/api/users/users.ts | 2 +- 11 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/GeneralComponents/AuthTypeIcon.tsx create mode 100644 frontend/src/components/GeneralComponents/EmailAndAuth.tsx diff --git a/frontend/src/components/AdminsComponents/AddAdmin.tsx b/frontend/src/components/AdminsComponents/AddAdmin.tsx index 3451e8da7..e73dbeb69 100644 --- a/frontend/src/components/AdminsComponents/AddAdmin.tsx +++ b/frontend/src/components/AdminsComponents/AddAdmin.tsx @@ -7,6 +7,7 @@ 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. @@ -162,7 +163,7 @@ export default function AddAdmin(props: { adminAdded: (user: User) => void }) { ); }} /> - {selected?.auth.email} + diff --git a/frontend/src/components/AdminsComponents/AdminListItem.tsx b/frontend/src/components/AdminsComponents/AdminListItem.tsx index 0cd4d3f62..5b081916e 100644 --- a/frontend/src/components/AdminsComponents/AdminListItem.tsx +++ b/frontend/src/components/AdminsComponents/AdminListItem.tsx @@ -1,6 +1,7 @@ import { User } from "../../utils/api/users/users"; import React from "react"; import { RemoveAdmin } from "./index"; +import { EmailAndAuth } from "../GeneralComponents"; /** * An item from [[AdminList]]. Contains the credentials of an admin and a button to remove the admin. @@ -11,7 +12,9 @@ export default function AdminItem(props: { admin: User; refresh: () => void }) { return ( {props.admin.name} - {props.admin.auth.email} + + + diff --git a/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx b/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx new file mode 100644 index 000000000..c79f8285c --- /dev/null +++ b/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx @@ -0,0 +1,19 @@ +import { HiOutlineMail } from "react-icons/hi"; +import { AiFillGithub, AiFillGoogleCircle, AiOutlineQuestionCircle } from "react-icons/ai"; + +/** + * An icon representing the type of authentication + * @param props.type email/github/google + */ +export default function AuthTypeIcon(props: { type: string }) { + console.log(props.type); + if (props.type === "email") { + return ; + } else if (props.type === "github") { + return ; + } else if (props.type === "google") { + return ; + } else { + return ; + } +} diff --git a/frontend/src/components/GeneralComponents/EmailAndAuth.tsx b/frontend/src/components/GeneralComponents/EmailAndAuth.tsx new file mode 100644 index 000000000..61fe38a72 --- /dev/null +++ b/frontend/src/components/GeneralComponents/EmailAndAuth.tsx @@ -0,0 +1,21 @@ +import { AuthTypeDiv, EmailAndAuthDiv, EmailDiv } from "./styles"; +import AuthTypeIcon from "./AuthTypeIcon"; +import { User } from "../../utils/api/users/users"; + +/** + * Email adress + auth type icon of a given user. + * @param props.user The given user. + */ +export default function EmailAndAuth(props: { user: User | undefined }) { + if (props.user === undefined) { + return null; + } + return ( + + + + + {props.user.auth.email} + + ); +} diff --git a/frontend/src/components/GeneralComponents/MenuItem.tsx b/frontend/src/components/GeneralComponents/MenuItem.tsx index c758c6859..849fd4fea 100644 --- a/frontend/src/components/GeneralComponents/MenuItem.tsx +++ b/frontend/src/components/GeneralComponents/MenuItem.tsx @@ -1,5 +1,6 @@ import { User } from "../../utils/api/users/users"; -import { NameDiv, EmailDiv } from "./styles"; +import { EmailDiv, NameDiv } from "./styles"; +import EmailAndAuth from "./EmailAndAuth"; /** * An item from a dropdown menu containing a user's name and email. @@ -9,7 +10,9 @@ export default function UserMenuItem(props: { user: User }) { return (
                                      {props.user.name} - {props.user.auth.email} + + +
                                      ); } diff --git a/frontend/src/components/GeneralComponents/index.ts b/frontend/src/components/GeneralComponents/index.ts index cf0669a1a..ba1b5d513 100644 --- a/frontend/src/components/GeneralComponents/index.ts +++ b/frontend/src/components/GeneralComponents/index.ts @@ -1 +1,3 @@ export { default as MenuItem } from "./MenuItem"; +export { default as AuthTypeIcon } from "./AuthTypeIcon"; +export { default as EmailAndAuth } from "./EmailAndAuth"; diff --git a/frontend/src/components/GeneralComponents/styles.ts b/frontend/src/components/GeneralComponents/styles.ts index 1020162d1..6c570862f 100644 --- a/frontend/src/components/GeneralComponents/styles.ts +++ b/frontend/src/components/GeneralComponents/styles.ts @@ -19,3 +19,12 @@ export const NameDiv = styled.div` export const EmailDiv = styled.div` float: right; `; + +export const AuthTypeDiv = styled.div` + float: right; + margin-left: 5px; +`; + +export const EmailAndAuthDiv = styled.div` + width: fit-content; +`; diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx index 25962feff..d3b0026e6 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/AddCoach.tsx @@ -7,6 +7,7 @@ import { AddAdminButton, ModalContentConfirm } from "../../../AdminsComponents/s import { AsyncTypeahead, Menu } from "react-bootstrap-typeahead"; import UserMenuItem from "../../../GeneralComponents/MenuItem"; import { StyledMenuItem } from "../../../GeneralComponents/styles"; +import { EmailAndAuth } from "../../../GeneralComponents"; /** * A button and popup to add a new coach to the given edition. @@ -148,7 +149,7 @@ export default function AddCoach(props: { edition: string; refreshCoaches: () => ); }} /> - {selected?.auth.email} + {addButton} diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx index c1ca8015b..0f0b6a094 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachListItem.tsx @@ -2,6 +2,7 @@ import { User } from "../../../../utils/api/users/users"; import React from "react"; import RemoveCoach from "./RemoveCoach"; import { RemoveTd } from "../styles"; +import { EmailAndAuth } from "../../../GeneralComponents"; /** * An item from [[CoachList]] which represents one coach. @@ -18,7 +19,9 @@ export default function CoachListItem(props: { return ( {props.coach.name} - {props.coach.auth.email} + + + {props.request.user.name} - {props.request.user.auth.email} + + + diff --git a/frontend/src/utils/api/users/users.ts b/frontend/src/utils/api/users/users.ts index 1c32cf8d5..d2b34b21e 100644 --- a/frontend/src/utils/api/users/users.ts +++ b/frontend/src/utils/api/users/users.ts @@ -8,7 +8,7 @@ export interface User { name: string; admin: boolean; auth: { - autType: string; + authType: string; email: string; }; } From 6f93b49f796c7eb40fe04f5fd2eb4ea52d09a598 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Thu, 14 Apr 2022 00:14:23 +0200 Subject: [PATCH 384/536] Fix Style --- .../src/components/UsersComponents/InviteUser/InviteUser.css | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/components/UsersComponents/InviteUser/InviteUser.css b/frontend/src/components/UsersComponents/InviteUser/InviteUser.css index bcbfb0d0f..2b087a3bc 100644 --- a/frontend/src/components/UsersComponents/InviteUser/InviteUser.css +++ b/frontend/src/components/UsersComponents/InviteUser/InviteUser.css @@ -1,4 +1,3 @@ - .email-field-error { border: 2px solid red !important; } From 16c866f76cb8fc427f658695b596ee618311f849 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Thu, 14 Apr 2022 00:24:42 +0200 Subject: [PATCH 385/536] Space out buttons on Remove Coach dialog --- .../Coaches/CoachesComponents/RemoveCoach.tsx | 10 +++++----- .../src/components/UsersComponents/Coaches/styles.ts | 6 +++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx index 6c14383ce..c7660e857 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/RemoveCoach.tsx @@ -5,7 +5,7 @@ import { removeCoachFromEdition, } from "../../../../utils/api/users/coaches"; import { Button, Modal, Spinner } from "react-bootstrap"; -import { ModalContent } from "../styles"; +import { DialogButton, ModalContent } from "../styles"; import { Error } from "../../Requests/styles"; /** @@ -64,22 +64,22 @@ export default function RemoveCoach(props: { } else { buttons = (
                                      - - + diff --git a/frontend/src/components/UsersComponents/Coaches/styles.ts b/frontend/src/components/UsersComponents/Coaches/styles.ts index f06d718e0..0d6f27b57 100644 --- a/frontend/src/components/UsersComponents/Coaches/styles.ts +++ b/frontend/src/components/UsersComponents/Coaches/styles.ts @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { Table } from "react-bootstrap"; +import { Button, Table } from "react-bootstrap"; export const CoachesContainer = styled.div` min-width: 450px; @@ -41,3 +41,7 @@ export const ListDiv = styled.div` overflow: auto; margin-top: 10px; `; + +export const DialogButton = styled(Button)` + margin-right: 4px; +`; From 8e227c759d59bb4b41bc099905aa34043cb8b44b Mon Sep 17 00:00:00 2001 From: beguille Date: Thu, 14 Apr 2022 10:01:47 +0200 Subject: [PATCH 386/536] fix test --- backend/src/app/routers/editions/editions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/routers/editions/editions.py b/backend/src/app/routers/editions/editions.py index f1c5ca790..839f12059 100644 --- a/backend/src/app/routers/editions/editions.py +++ b/backend/src/app/routers/editions/editions.py @@ -43,7 +43,7 @@ async def get_editions(db: Session = Depends(get_session), user: User = Depends( EditionList: an object with a list of all the editions. """ if user.admin: - return logic_editions.get_editions(db, page) + return logic_editions.get_editions_page(db, page) else: return EditionList(editions=user.editions) From 71446d2561351562543293db2254e44e7bd463fa Mon Sep 17 00:00:00 2001 From: beguille Date: Thu, 14 Apr 2022 10:05:50 +0200 Subject: [PATCH 387/536] fix lint --- backend/src/app/routers/editions/editions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/app/routers/editions/editions.py b/backend/src/app/routers/editions/editions.py index 839f12059..0c5e4acc3 100644 --- a/backend/src/app/routers/editions/editions.py +++ b/backend/src/app/routers/editions/editions.py @@ -44,8 +44,8 @@ async def get_editions(db: Session = Depends(get_session), user: User = Depends( """ if user.admin: return logic_editions.get_editions_page(db, page) - else: - return EditionList(editions=user.editions) + + return EditionList(editions=user.editions) @editions_router.get("/{edition_name}", response_model=Edition, tags=[Tags.EDITIONS], From 6e315b0b86bef1a70d5c07f48d43292ea8677f28 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Thu, 14 Apr 2022 23:59:55 +0200 Subject: [PATCH 388/536] used CommonQueryParams in crud --- backend/src/app/logic/students.py | 4 +--- backend/src/database/crud/students.py | 12 +++++------ .../test_database/test_crud/test_students.py | 21 ++++++++++++------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index cb9e634f1..326fa278d 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -30,9 +30,7 @@ def get_students_search(db: Session, edition: Edition, commons: CommonQueryParam return ReturnStudentList(students=[]) else: skills = [] - students_orm = get_students(db, edition, first_name=commons.first_name, - last_name=commons.last_name, alumni=commons.alumni, - student_coach=commons.student_coach, skills=skills) + students_orm = get_students(db, edition, commons, skills) students: list[StudentModel] = [] for student in students_orm: diff --git a/backend/src/database/crud/students.py b/backend/src/database/crud/students.py index d59f717d8..723e974d6 100644 --- a/backend/src/database/crud/students.py +++ b/backend/src/database/crud/students.py @@ -1,6 +1,7 @@ from sqlalchemy.orm import Session from src.database.enums import DecisionEnum from src.database.models import Edition, Skill, Student, DecisionEmail +from src.app.schemas.students import CommonQueryParams def get_student_by_id(db: Session, student_id: int) -> Student: @@ -22,18 +23,17 @@ def delete_student(db: Session, student: Student) -> None: db.commit() -def get_students(db: Session, edition: Edition, first_name: str = "", last_name: str = "", alumni: bool = False, - student_coach: bool = False, skills: list[Skill] = None) -> list[Student]: +def get_students(db: Session, edition: Edition, commons: CommonQueryParams, skills: list[Skill] = None) -> list[Student]: """Get students""" query = db.query(Student)\ .where(Student.edition == edition)\ - .where(Student.first_name.contains(first_name))\ - .where(Student.last_name.contains(last_name))\ + .where(Student.first_name.contains(commons.first_name))\ + .where(Student.last_name.contains(commons.last_name))\ - if alumni: + if commons.alumni: query = query.where(Student.alumni) - if student_coach: + if commons.student_coach: query = query.where(Student.wants_to_be_student_coach) if skills is None: diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index 2eee76333..ccf4140e1 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -6,6 +6,7 @@ from src.database.enums import DecisionEnum from src.database.crud.students import (get_student_by_id, set_definitive_decision_on_student, delete_student, get_students, get_emails) +from src.app.schemas.students import CommonQueryParams @pytest.fixture @@ -112,7 +113,7 @@ def test_get_all_students(database_with_data: Session): """test get all students""" edition: Edition = database_with_data.query( Edition).where(Edition.edition_id == 1).one() - students = get_students(database_with_data, edition) + students = get_students(database_with_data, edition, CommonQueryParams()) assert len(students) == 2 @@ -120,7 +121,8 @@ def test_search_students_on_first_name(database_with_data: Session): """test""" edition: Edition = database_with_data.query( Edition).where(Edition.edition_id == 1).one() - students = get_students(database_with_data, edition, first_name="Jos") + students = get_students(database_with_data, edition, + CommonQueryParams(first_name="Jos")) assert len(students) == 1 @@ -128,7 +130,8 @@ def test_search_students_on_last_name(database_with_data: Session): """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, last_name="Vermeulen") + students = get_students(database_with_data, edition, + CommonQueryParams(last_name="Vermeulen")) assert len(students) == 1 @@ -136,7 +139,8 @@ def test_search_students_alumni(database_with_data: Session): """tests search on alumni""" edition: Edition = database_with_data.query( Edition).where(Edition.edition_id == 1).one() - students = get_students(database_with_data, edition, alumni=True) + students = get_students(database_with_data, edition, + CommonQueryParams(alumni=True)) assert len(students) == 1 @@ -144,7 +148,8 @@ def test_search_students_student_coach(database_with_data: Session): """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, student_coach=True) + students = get_students(database_with_data, edition, + CommonQueryParams(student_coach=True)) assert len(students) == 1 @@ -154,7 +159,8 @@ def test_search_students_one_skill(database_with_data: Session): 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, skills=[skill]) + students = get_students(database_with_data, edition, + CommonQueryParams(), skills=[skill]) assert len(students) == 1 @@ -164,7 +170,8 @@ def test_search_students_multiple_skills(database_with_data: Session): 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, skills=skills) + students = get_students(database_with_data, edition, + CommonQueryParams(), skills=skills) assert len(students) == 1 From 29efbb38c9db8dac3cb0851b5d86825d9506660a Mon Sep 17 00:00:00 2001 From: stijndcl Date: Wed, 13 Apr 2022 16:51:03 +0200 Subject: [PATCH 389/536] Add new statuses to the email status enum --- ...e98fe039_create_enum_for_email_statuses.py | 33 +++++++++++++++++++ backend/src/database/enums.py | 17 ++++++++++ backend/src/database/models.py | 4 +-- backend/tests/fill_database.py | 16 ++++----- 4 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 backend/migrations/versions/43e6e98fe039_create_enum_for_email_statuses.py diff --git a/backend/migrations/versions/43e6e98fe039_create_enum_for_email_statuses.py b/backend/migrations/versions/43e6e98fe039_create_enum_for_email_statuses.py new file mode 100644 index 000000000..8973102ab --- /dev/null +++ b/backend/migrations/versions/43e6e98fe039_create_enum_for_email_statuses.py @@ -0,0 +1,33 @@ +"""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) + # ### 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) + # ### end Alembic commands ### diff --git a/backend/src/database/enums.py b/backend/src/database/enums.py index bcd6a4762..ec76d63d6 100644 --- a/backend/src/database/enums.py +++ b/backend/src/database/enums.py @@ -14,6 +14,23 @@ class DecisionEnum(enum.Enum): NO = 3 +@enum.unique +class EmailStatusEnum(enum.Enum): + """Enum for the status attached to an email sent to a student""" + # Nothing happened (undecided/screening) + APPLIED = 0 + # We're looking for a project (maybe) + AWAITING_PROJECT = 1 + # Can participate (yes) + APPROVED = 2 + # Student signed the contract + CONTRACT_CONFIRMED = 3 + # Student indicated they don't want to participate anymore + CONTRACT_DECLINED = 4 + # We've rejected the student ourselves (no) + REJECTED = 5 + + @enum.unique class RoleEnum(enum.Enum): """Enum for the different roles a user can have""" diff --git a/backend/src/database/models.py b/backend/src/database/models.py index 802cd5fa1..ebf37a49e 100644 --- a/backend/src/database/models.py +++ b/backend/src/database/models.py @@ -17,7 +17,7 @@ from sqlalchemy.orm import declarative_base, relationship from sqlalchemy_utils import UUIDType # type: ignore -from src.database.enums import DecisionEnum, QuestionEnum +from src.database.enums import DecisionEnum, EmailStatusEnum, QuestionEnum Base = declarative_base() @@ -77,7 +77,7 @@ class DecisionEmail(Base): email_id = Column(Integer, primary_key=True) student_id = Column(Integer, ForeignKey("students.student_id"), nullable=False) - decision = Column(Enum(DecisionEnum), nullable=False) + decision = Column(Enum(EmailStatusEnum), nullable=False) date = Column(DateTime, nullable=False) student: Student = relationship("Student", back_populates="emails", uselist=False) diff --git a/backend/tests/fill_database.py b/backend/tests/fill_database.py index b124dcada..930e2577a 100644 --- a/backend/tests/fill_database.py +++ b/backend/tests/fill_database.py @@ -4,7 +4,7 @@ from src.database.models import (User, AuthEmail, Skill, Student, Edition, CoachRequest, DecisionEmail, InviteLink, Partner, Project, ProjectRole, Suggestion) -from src.database.enums import DecisionEnum +from src.database.enums import DecisionEnum, EmailStatusEnum from src.app.logic.security import get_password_hash @@ -188,19 +188,19 @@ def fill_database(db: Session): # DecisionEmail decision_email1: DecisionEmail = DecisionEmail( - decision=DecisionEnum.NO, student=student29, date=date.today()) + decision=EmailStatusEnum.REJECTED, student=student29, date=date.today()) decision_email2: DecisionEmail = DecisionEmail( - decision=DecisionEnum.YES, student=student09, date=date.today()) + decision=EmailStatusEnum.APPROVED, student=student09, date=date.today()) decision_email3: DecisionEmail = DecisionEmail( - decision=DecisionEnum.YES, student=student10, date=date.today()) + decision=EmailStatusEnum.APPROVED, student=student10, date=date.today()) decision_email4: DecisionEmail = DecisionEmail( - decision=DecisionEnum.YES, student=student11, date=date.today()) + decision=EmailStatusEnum.APPROVED, student=student11, date=date.today()) decision_email5: DecisionEmail = DecisionEmail( - decision=DecisionEnum.YES, student=student12, date=date.today()) + decision=EmailStatusEnum.APPROVED, student=student12, date=date.today()) decision_email6: DecisionEmail = DecisionEmail( - decision=DecisionEnum.MAYBE, student=student06, date=date.today()) + decision=EmailStatusEnum.AWAITING_PROJECT, student=student06, date=date.today()) decision_email7: DecisionEmail = DecisionEmail( - decision=DecisionEnum.MAYBE, student=student26, date=date.today()) + decision=EmailStatusEnum.AWAITING_PROJECT, student=student26, date=date.today()) db.add(decision_email1) db.add(decision_email2) db.add(decision_email3) From 7741f41dd53b1ee1026b8c1bd814aa0ea9b4cfe9 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sat, 16 Apr 2022 14:37:57 +0200 Subject: [PATCH 390/536] Encode edition name in backend --- backend/src/app/logic/invites.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/src/app/logic/invites.py b/backend/src/app/logic/invites.py index 85ed867d7..44a1d55be 100644 --- a/backend/src/app/logic/invites.py +++ b/backend/src/app/logic/invites.py @@ -1,3 +1,5 @@ +import base64 + from sqlalchemy.orm import Session import settings @@ -31,8 +33,12 @@ def create_mailto_link(db: Session, edition: Edition, email_address: EmailAddres # Create db entry new_link_db = crud.create_invite_link(db, edition, email_address.email) + # Add edition name & encode with base64 + encoded_uuid = f"{new_link_db.edition.name}/{new_link_db.uuid}".encode("ascii") + encoded_link = base64.b64encode(encoded_uuid).decode("ascii") + # Create endpoint for the user to click on - link = f"{settings.FRONTEND_URL}/register/{new_link_db.uuid}" + link = f"{settings.FRONTEND_URL}/register/{encoded_link}" return NewInviteLink(mail_to=generate_mailto_string( recipient=email_address.email, subject=f"Open Summer Of Code {edition.year} invitation", From e0ba1eb714c77611806635e5b3f3639e6cbfa3e5 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sat, 16 Apr 2022 14:57:51 +0200 Subject: [PATCH 391/536] Decode edition name in frontend --- frontend/package.json | 1 + frontend/src/utils/logic/registration.ts | 30 +++++++++++++++++++ .../src/views/RegisterPage/RegisterPage.tsx | 23 +++++++------- frontend/yarn.lock | 18 +++++++++++ 4 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 frontend/src/utils/logic/registration.ts diff --git a/frontend/package.json b/frontend/package.json index 2e9ba6cd8..5b835baaf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,7 @@ "@fortawesome/react-fontawesome": "^0.1.17", "axios": "^0.26.1", "bootstrap": "5.1.3", + "buffer": "^6.0.3", "react": "^17.0.2", "react-bootstrap": "^2.2.1", "react-dom": "^17.0.2", diff --git a/frontend/src/utils/logic/registration.ts b/frontend/src/utils/logic/registration.ts new file mode 100644 index 000000000..d6d03a42c --- /dev/null +++ b/frontend/src/utils/logic/registration.ts @@ -0,0 +1,30 @@ +const Buffer = require("buffer/").Buffer; + +/** + * Decode a base64-encoded registration link + */ +export function decodeRegistrationLink( + url: string | undefined +): { edition: string; uuid: string } | null { + if (!url) return null; + + // Base64 decode + const decoded = Buffer.from(url, "base64").toString(); + + // Invalid link + if (!decoded.includes("/")) { + return null; + } + + try { + const [edition, uuid] = decoded.split("/"); + + return { + edition: edition, + uuid: uuid, + }; + } catch (e) { + console.error(e); + return null; + } +} diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index 761e28f05..014279db8 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -15,22 +15,23 @@ import { } from "../../components/RegisterComponents"; import { RegisterFormContainer, Or, RegisterButton } from "./styles"; +import { decodeRegistrationLink } from "../../utils/logic/registration"; /** * 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 [validUuid, setUuid] = useState(false); + const [validUuid, setValidUuid] = useState(false); const params = useParams(); - const uuid = params.uuid; + const data = decodeRegistrationLink(params.uuid); useEffect(() => { async function validateUuid() { - if (uuid) { - const response = await validateRegistrationUrl("1", uuid); + if (data) { + const response = await validateRegistrationUrl(data.edition, data.uuid); if (response) { - setUuid(true); + setValidUuid(true); } } } @@ -39,7 +40,7 @@ export default function RegisterPage() { } }); - async function callRegister(uuid: string) { + async function callRegister(edition: string, uuid: string) { // Check if passwords are the same if (password !== confirmPassword) { alert("Passwords do not match"); @@ -51,8 +52,6 @@ export default function RegisterPage() { return; } - // TODO this has to change to get the edition the invite belongs to - const edition = "ed2022"; try { const response = await register(edition, email, name, uuid, password); if (response) { @@ -71,7 +70,7 @@ export default function RegisterPage() { const navigate = useNavigate(); - if (validUuid && uuid) { + if (validUuid && data) { return (
                                      @@ -84,10 +83,12 @@ export default function RegisterPage() { callRegister(uuid)} + callRegister={() => callRegister(data.edition, data.uuid)} />
                                      - callRegister(uuid)}>Register + callRegister(data.edition, data.uuid)}> + Register +
                                      diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 1fb299499..db25e1b10 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2847,6 +2847,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 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" + 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" @@ -2970,6 +2975,14 @@ buffer-indexof@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" 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" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + builtin-modules@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" @@ -5155,6 +5168,11 @@ identity-obj-proxy@^3.0.0: 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" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^5.1.1, ignore@^5.1.8, ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" From 77660036b52eca6f3d4d10320696f10e61c53ae8 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Sat, 16 Apr 2022 16:41:29 +0200 Subject: [PATCH 392/536] add refresh token to localstorage --- frontend/src/components/navbar/NavBar.tsx | 6 +-- frontend/src/contexts/auth-context.tsx | 40 ++++++++++++++----- frontend/src/data/enums/local-storage.ts | 3 +- frontend/src/utils/api/api.ts | 7 ++++ frontend/src/utils/api/auth.ts | 7 ++++ frontend/src/utils/api/login.ts | 6 ++- frontend/src/utils/local-storage/auth.ts | 37 +++++++++++++---- frontend/src/utils/local-storage/index.ts | 2 +- .../VerifyingTokenPage/VerifyingTokenPage.tsx | 7 ++-- 9 files changed, 88 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/navbar/NavBar.tsx b/frontend/src/components/navbar/NavBar.tsx index eccd944c1..202293568 100644 --- a/frontend/src/components/navbar/NavBar.tsx +++ b/frontend/src/components/navbar/NavBar.tsx @@ -8,8 +8,8 @@ import { useAuth } from "../../contexts/auth-context"; * Links are hidden if the user is not authorized to see them. */ export default function NavBar() { - const { token, setToken } = useAuth(); - const hidden = token ? "nav-links" : "nav-hidden"; + const { accessToken, setAccessToken } = useAuth(); + const hidden = accessToken ? "nav-links" : "nav-hidden"; return ( <> @@ -32,7 +32,7 @@ export default function NavBar() { { - setToken(null); + setAccessToken(null); }} > Log out diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx index 03b94021b..d8e70307d 100644 --- a/frontend/src/contexts/auth-context.tsx +++ b/frontend/src/contexts/auth-context.tsx @@ -1,7 +1,11 @@ /** Context hook to maintain the authentication state of the user **/ import { Role } from "../data/enums"; import React, { useContext, ReactNode, useState } from "react"; -import { getToken, setToken as setTokenInStorage } from "../utils/local-storage"; +import { + getAccessToken, getRefreshToken, + setAccessToken as setAccessTokenInStorage, + setRefreshToken as setRefreshTokenInStorage +} from "../utils/local-storage"; /** * Interface that holds the data stored in the AuthContext. @@ -13,8 +17,10 @@ export interface AuthContextState { setRole: (value: Role | null) => void; userId: number | null; setUserId: (value: number | null) => void; - token: string | null; - setToken: (value: string | null) => void; + accessToken: string | null; + setAccessToken: (value: string | null) => void; + refreshToken: string | null; + setRefreshToken: (value: string | null) => void; editions: string[]; setEditions: (value: string[]) => void; } @@ -32,8 +38,10 @@ function authDefaultState(): AuthContextState { setRole: (_: Role | null) => {}, userId: null, setUserId: (value: number | null) => {}, - token: getToken(), - setToken: (_: string | null) => {}, + accessToken: getAccessToken(), + setAccessToken: (_: string | null) => {}, + refreshToken: getRefreshToken(), + setRefreshToken: (_: string | null) => {}, editions: [], setEditions: (_: string[]) => {}, }; @@ -61,7 +69,8 @@ export function AuthProvider({ children }: { children: ReactNode }) { const [editions, setEditions] = useState([]); const [userId, setUserId] = useState(null); // Default value: check LocalStorage - const [token, setToken] = useState(getToken()); + const [accessToken, setAccessToken] = useState(getAccessToken()); + const [refreshToken, setRefreshToken] = useState(getRefreshToken()); // Create AuthContext value const authContextValue: AuthContextState = { @@ -71,16 +80,27 @@ export function AuthProvider({ children }: { children: ReactNode }) { setRole: setRole, userId: userId, setUserId: setUserId, - token: token, - setToken: (value: string | null) => { + accessToken: accessToken, + setAccessToken: (value: string | null) => { // Log the user out if token is null if (value === null) { setIsLoggedIn(false); } // Set the token in LocalStorage - setTokenInStorage(value); - setToken(value); + setAccessTokenInStorage(value); + setAccessToken(value); + }, + refreshToken: refreshToken, + setRefreshToken: (value: string | null) => { + // Log the user out if token is null + if (value === null) { + setIsLoggedIn(false); + } + + // Set the token in LocalStorage + setRefreshTokenInStorage(value); + setRefreshToken(value); }, editions: editions, setEditions: setEditions, diff --git a/frontend/src/data/enums/local-storage.ts b/frontend/src/data/enums/local-storage.ts index 3b2765eb2..a9a8c5038 100644 --- a/frontend/src/data/enums/local-storage.ts +++ b/frontend/src/data/enums/local-storage.ts @@ -5,5 +5,6 @@ export const enum StorageKey { /** * Bearer token used to authorize the user's requests in the backend. */ - BEARER_TOKEN = "bearerToken", + ACCESS_TOKEN = "accessToken", + REFRESH_TOKEN = "refreshToken", } diff --git a/frontend/src/utils/api/api.ts b/frontend/src/utils/api/api.ts index 2995a16e4..7374d34ca 100644 --- a/frontend/src/utils/api/api.ts +++ b/frontend/src/utils/api/api.ts @@ -4,6 +4,13 @@ import { BASE_URL } from "../../settings"; export const axiosInstance = axios.create(); axiosInstance.defaults.baseURL = BASE_URL; +axiosInstance.interceptors.response.use(response => { + return response +}, error => { + /* TODO: refresh token */ + return axiosInstance(error.config) +}) + /** * Function to set the default bearer token in the request headers. * Passing `null` as the value will remove the header instead. diff --git a/frontend/src/utils/api/auth.ts b/frontend/src/utils/api/auth.ts index 2d3712d43..5af463b3b 100644 --- a/frontend/src/utils/api/auth.ts +++ b/frontend/src/utils/api/auth.ts @@ -19,6 +19,7 @@ export async function validateBearerToken(token: string | null): Promise 404 page als niet ingelogd +* -> swagger werkt weer +* -> async dingen +* */ diff --git a/frontend/src/utils/api/login.ts b/frontend/src/utils/api/login.ts index 7570b4aa1..16ec6bd23 100644 --- a/frontend/src/utils/api/login.ts +++ b/frontend/src/utils/api/login.ts @@ -4,7 +4,8 @@ import { AuthContextState } from "../../contexts"; import { Role } from "../../data/enums"; interface LoginResponse { - accessToken: string; + access_token: string; + refresh_token: string; user: { admin: boolean; editions: string[]; @@ -27,7 +28,8 @@ export async function logIn(auth: AuthContextState, email: string, password: str try { const response = await axiosInstance.post("/login/token", payload); const login = response.data as LoginResponse; - auth.setToken(login.accessToken); + auth.setAccessToken(login.access_token); + auth.setRefreshToken(login.refresh_token); auth.setIsLoggedIn(true); auth.setRole(login.user.admin ? Role.ADMIN : Role.COACH); auth.setUserId(login.user.userId); diff --git a/frontend/src/utils/local-storage/auth.ts b/frontend/src/utils/local-storage/auth.ts index 86d75d3f6..cfd154b83 100644 --- a/frontend/src/utils/local-storage/auth.ts +++ b/frontend/src/utils/local-storage/auth.ts @@ -1,13 +1,24 @@ -import { StorageKey } from "../../data/enums"; +import {StorageKey} from "../../data/enums"; /** - * Function to set a new value for the bearer token in LocalStorage. + * Function to set a new value for the access token in LocalStorage. */ -export function setToken(value: string | null) { +export function setAccessToken(value: string | null) { + setToken(StorageKey.ACCESS_TOKEN, value); +} + +/** + * Function to set a new value for the refresh token in LocalStorage. + */ +export function setRefreshToken(value: string | null) { + setToken(StorageKey.REFRESH_TOKEN, value); +} + +function setToken(key: StorageKey, value: string | null) { if (value === null) { - localStorage.removeItem(StorageKey.BEARER_TOKEN); + localStorage.removeItem(key); } else { - localStorage.setItem(StorageKey.BEARER_TOKEN, value); + localStorage.setItem(key, value); } } @@ -15,6 +26,18 @@ export function setToken(value: string | null) { * Function to pull the user's token out of LocalStorage. * Returns `null` if there is no token in LocalStorage yet. */ -export function getToken(): string | null { - return localStorage.getItem(StorageKey.BEARER_TOKEN); +export function getAccessToken(): string | null { + return getToken(StorageKey.ACCESS_TOKEN) +} + +/** + * Function to pull the user's token out of LocalStorage. + * Returns `null` if there is no token in LocalStorage yet. + */ +export function getRefreshToken(): string | null { + return getToken(StorageKey.REFRESH_TOKEN) +} + +function getToken(key: StorageKey) { + return localStorage.getItem(key); } diff --git a/frontend/src/utils/local-storage/index.ts b/frontend/src/utils/local-storage/index.ts index 29a2355c0..2583f0286 100644 --- a/frontend/src/utils/local-storage/index.ts +++ b/frontend/src/utils/local-storage/index.ts @@ -1 +1 @@ -export { getToken, setToken } from "./auth"; +export { getAccessToken, getRefreshToken, setAccessToken, setRefreshToken } from "./auth"; diff --git a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx index 6c95c7afc..c95660a53 100644 --- a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx +++ b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx @@ -14,16 +14,17 @@ export default function VerifyingTokenPage() { useEffect(() => { const verifyToken = async () => { - const response = await validateBearerToken(authContext.token); + const response = await validateBearerToken(authContext.accessToken); if (response === null) { - authContext.setToken(null); + authContext.setAccessToken(null); + authContext.setRefreshToken(null); authContext.setIsLoggedIn(false); authContext.setRole(null); authContext.setEditions([]); } else { // Token was valid, use it as the default request header - setBearerToken(authContext.token); + setBearerToken(authContext.accessToken); authContext.setIsLoggedIn(true); authContext.setRole(response.admin ? Role.ADMIN : Role.COACH); authContext.setUserId(response.userId); From 6d086f1b2d078fb21c04c498ac6bfd442e58c5b3 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 16 Apr 2022 19:05:20 +0200 Subject: [PATCH 393/536] Created the route for a new project --- frontend/src/Router.tsx | 4 ++-- frontend/src/views/index.ts | 3 +-- .../CreateProjectPage/CreateProjectPage.tsx | 8 +++++++ .../projectViews/CreateProjectPage/index.ts | 1 + .../projectViews/CreateProjectPage/styles.ts | 3 +++ .../ProjectsPage/ProjectsPage.tsx | 21 ++++++++++++++++--- frontend/src/views/projectViews/index.ts | 3 ++- 7 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx create mode 100644 frontend/src/views/projectViews/CreateProjectPage/index.ts create mode 100644 frontend/src/views/projectViews/CreateProjectPage/styles.ts diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index eec3fd9ef..38f2fea9a 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -7,7 +7,7 @@ import { BrowserRouter, Navigate, Outlet, Route, Routes } from "react-router-dom import RegisterPage from "./views/RegisterPage"; import StudentsPage from "./views/StudentsPage/StudentsPage"; import UsersPage from "./views/UsersPage"; -import { ProjectsPage, ProjectDetailPage } from "./views"; +import { ProjectsPage, ProjectDetailPage, CreateProjectPage } from "./views"; import PendingPage from "./views/PendingPage"; import Footer from "./components/Footer"; import { useAuth } from "./contexts/auth-context"; @@ -60,7 +60,7 @@ export default function Router() { } /> }> {/* TODO create project page */} - } /> + } /> {/* TODO project page */} } /> diff --git a/frontend/src/views/index.ts b/frontend/src/views/index.ts index 137777e61..7394eff11 100644 --- a/frontend/src/views/index.ts +++ b/frontend/src/views/index.ts @@ -1,8 +1,7 @@ export * as Errors from "./errors"; export { default as LoginPage } from "./LoginPage"; export { default as PendingPage } from "./PendingPage"; -export { ProjectsPage } from "./projectViews"; -export { ProjectDetailPage } from "./projectViews" +export { ProjectsPage, ProjectDetailPage, CreateProjectPage } from "./projectViews"; export { default as RegisterPage } from "./RegisterPage"; export { default as StudentsPage } from "./StudentsPage"; export { default as UsersPage } from "./UsersPage"; diff --git a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx new file mode 100644 index 000000000..74fb95142 --- /dev/null +++ b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx @@ -0,0 +1,8 @@ +export default function CreateProjectPage() { + + return( +
                                      + +
                                      + ) +} diff --git a/frontend/src/views/projectViews/CreateProjectPage/index.ts b/frontend/src/views/projectViews/CreateProjectPage/index.ts new file mode 100644 index 000000000..f20b5ad36 --- /dev/null +++ b/frontend/src/views/projectViews/CreateProjectPage/index.ts @@ -0,0 +1 @@ +export { default } from "./CreateProjectPage"; diff --git a/frontend/src/views/projectViews/CreateProjectPage/styles.ts b/frontend/src/views/projectViews/CreateProjectPage/styles.ts new file mode 100644 index 000000000..8e0438e88 --- /dev/null +++ b/frontend/src/views/projectViews/CreateProjectPage/styles.ts @@ -0,0 +1,3 @@ +import styled from "styled-components"; + +export const CreateProjectContainer = styled.div``; diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index 4e42a80cb..01314f2ad 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -4,16 +4,27 @@ import { ProjectCard } from "../../../components/ProjectsComponents"; import { CardsGrid, CreateButton, SearchButton, SearchField, OwnProject } from "./styles"; import { useAuth } from "../../../contexts/auth-context"; import { Project } from "../../../data/interfaces"; +import { useNavigate } from "react-router-dom"; +/** + * + * @returns The projects overview page where you can see all the projects. + * You can filter on your own projects or filter on project name. + * + */ function ProjectPage() { const [projectsAPI, setProjectsAPI] = useState>([]); - const [projects, setProjects] = useState>([]); const [gotProjects, setGotProjects] = useState(false); + // To filter projects we need to keep a separate list to avoid calling the API every time we change te filters. + const [projects, setProjects] = useState>([]); + + // Keep track of the set filters const [searchString, setSearchString] = useState(""); const [ownProjects, setOwnProjects] = useState(false); - const { userId } = useAuth(); + const navigate = useNavigate(); + const { userId, role } = useAuth(); /** * Uses to filter the results based onto search string and own projects @@ -66,7 +77,11 @@ function ProjectPage() { placeholder="project name" /> Search - Create Project + {role === 0 ? ( + navigate("/editions/summerof2022/projects/new")}> + Create Project + + ) : null}
                                      Date: Sat, 16 Apr 2022 19:07:19 +0200 Subject: [PATCH 394/536] Created the route for a new project --- frontend/src/Router.tsx | 4 ++-- frontend/src/views/index.ts | 3 +-- .../CreateProjectPage/CreateProjectPage.tsx | 8 +++++++ .../projectViews/CreateProjectPage/index.ts | 1 + .../projectViews/CreateProjectPage/styles.ts | 3 +++ .../ProjectsPage/ProjectsPage.tsx | 21 ++++++++++++++++--- frontend/src/views/projectViews/index.ts | 3 ++- 7 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx create mode 100644 frontend/src/views/projectViews/CreateProjectPage/index.ts create mode 100644 frontend/src/views/projectViews/CreateProjectPage/styles.ts diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index eec3fd9ef..38f2fea9a 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -7,7 +7,7 @@ import { BrowserRouter, Navigate, Outlet, Route, Routes } from "react-router-dom import RegisterPage from "./views/RegisterPage"; import StudentsPage from "./views/StudentsPage/StudentsPage"; import UsersPage from "./views/UsersPage"; -import { ProjectsPage, ProjectDetailPage } from "./views"; +import { ProjectsPage, ProjectDetailPage, CreateProjectPage } from "./views"; import PendingPage from "./views/PendingPage"; import Footer from "./components/Footer"; import { useAuth } from "./contexts/auth-context"; @@ -60,7 +60,7 @@ export default function Router() { } /> }> {/* TODO create project page */} - } /> + } /> {/* TODO project page */} } /> diff --git a/frontend/src/views/index.ts b/frontend/src/views/index.ts index 137777e61..7394eff11 100644 --- a/frontend/src/views/index.ts +++ b/frontend/src/views/index.ts @@ -1,8 +1,7 @@ export * as Errors from "./errors"; export { default as LoginPage } from "./LoginPage"; export { default as PendingPage } from "./PendingPage"; -export { ProjectsPage } from "./projectViews"; -export { ProjectDetailPage } from "./projectViews" +export { ProjectsPage, ProjectDetailPage, CreateProjectPage } from "./projectViews"; export { default as RegisterPage } from "./RegisterPage"; export { default as StudentsPage } from "./StudentsPage"; export { default as UsersPage } from "./UsersPage"; diff --git a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx new file mode 100644 index 000000000..74fb95142 --- /dev/null +++ b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx @@ -0,0 +1,8 @@ +export default function CreateProjectPage() { + + return( +
                                      + +
                                      + ) +} diff --git a/frontend/src/views/projectViews/CreateProjectPage/index.ts b/frontend/src/views/projectViews/CreateProjectPage/index.ts new file mode 100644 index 000000000..f20b5ad36 --- /dev/null +++ b/frontend/src/views/projectViews/CreateProjectPage/index.ts @@ -0,0 +1 @@ +export { default } from "./CreateProjectPage"; diff --git a/frontend/src/views/projectViews/CreateProjectPage/styles.ts b/frontend/src/views/projectViews/CreateProjectPage/styles.ts new file mode 100644 index 000000000..8e0438e88 --- /dev/null +++ b/frontend/src/views/projectViews/CreateProjectPage/styles.ts @@ -0,0 +1,3 @@ +import styled from "styled-components"; + +export const CreateProjectContainer = styled.div``; diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index 4e42a80cb..01314f2ad 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -4,16 +4,27 @@ import { ProjectCard } from "../../../components/ProjectsComponents"; import { CardsGrid, CreateButton, SearchButton, SearchField, OwnProject } from "./styles"; import { useAuth } from "../../../contexts/auth-context"; import { Project } from "../../../data/interfaces"; +import { useNavigate } from "react-router-dom"; +/** + * + * @returns The projects overview page where you can see all the projects. + * You can filter on your own projects or filter on project name. + * + */ function ProjectPage() { const [projectsAPI, setProjectsAPI] = useState>([]); - const [projects, setProjects] = useState>([]); const [gotProjects, setGotProjects] = useState(false); + // To filter projects we need to keep a separate list to avoid calling the API every time we change te filters. + const [projects, setProjects] = useState>([]); + + // Keep track of the set filters const [searchString, setSearchString] = useState(""); const [ownProjects, setOwnProjects] = useState(false); - const { userId } = useAuth(); + const navigate = useNavigate(); + const { userId, role } = useAuth(); /** * Uses to filter the results based onto search string and own projects @@ -66,7 +77,11 @@ function ProjectPage() { placeholder="project name" /> Search - Create Project + {role === 0 ? ( + navigate("/editions/summerof2022/projects/new")}> + Create Project + + ) : null}
                                      Date: Sat, 16 Apr 2022 19:13:28 +0200 Subject: [PATCH 395/536] Revert "Created the route for a new project" This reverts commit 6d086f1b2d078fb21c04c498ac6bfd442e58c5b3. --- frontend/src/Router.tsx | 4 ++-- frontend/src/views/index.ts | 3 ++- .../CreateProjectPage/CreateProjectPage.tsx | 8 ------- .../projectViews/CreateProjectPage/index.ts | 1 - .../projectViews/CreateProjectPage/styles.ts | 3 --- .../ProjectsPage/ProjectsPage.tsx | 21 +++---------------- frontend/src/views/projectViews/index.ts | 3 +-- 7 files changed, 8 insertions(+), 35 deletions(-) delete mode 100644 frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx delete mode 100644 frontend/src/views/projectViews/CreateProjectPage/index.ts delete mode 100644 frontend/src/views/projectViews/CreateProjectPage/styles.ts diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 38f2fea9a..eec3fd9ef 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -7,7 +7,7 @@ import { BrowserRouter, Navigate, Outlet, Route, Routes } from "react-router-dom import RegisterPage from "./views/RegisterPage"; import StudentsPage from "./views/StudentsPage/StudentsPage"; import UsersPage from "./views/UsersPage"; -import { ProjectsPage, ProjectDetailPage, CreateProjectPage } from "./views"; +import { ProjectsPage, ProjectDetailPage } from "./views"; import PendingPage from "./views/PendingPage"; import Footer from "./components/Footer"; import { useAuth } from "./contexts/auth-context"; @@ -60,7 +60,7 @@ export default function Router() { } /> }> {/* TODO create project page */} - } /> + } /> {/* TODO project page */} } /> diff --git a/frontend/src/views/index.ts b/frontend/src/views/index.ts index 7394eff11..137777e61 100644 --- a/frontend/src/views/index.ts +++ b/frontend/src/views/index.ts @@ -1,7 +1,8 @@ export * as Errors from "./errors"; export { default as LoginPage } from "./LoginPage"; export { default as PendingPage } from "./PendingPage"; -export { ProjectsPage, ProjectDetailPage, CreateProjectPage } from "./projectViews"; +export { ProjectsPage } from "./projectViews"; +export { ProjectDetailPage } from "./projectViews" export { default as RegisterPage } from "./RegisterPage"; export { default as StudentsPage } from "./StudentsPage"; export { default as UsersPage } from "./UsersPage"; diff --git a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx deleted file mode 100644 index 74fb95142..000000000 --- a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export default function CreateProjectPage() { - - return( -
                                      - -
                                      - ) -} diff --git a/frontend/src/views/projectViews/CreateProjectPage/index.ts b/frontend/src/views/projectViews/CreateProjectPage/index.ts deleted file mode 100644 index f20b5ad36..000000000 --- a/frontend/src/views/projectViews/CreateProjectPage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./CreateProjectPage"; diff --git a/frontend/src/views/projectViews/CreateProjectPage/styles.ts b/frontend/src/views/projectViews/CreateProjectPage/styles.ts deleted file mode 100644 index 8e0438e88..000000000 --- a/frontend/src/views/projectViews/CreateProjectPage/styles.ts +++ /dev/null @@ -1,3 +0,0 @@ -import styled from "styled-components"; - -export const CreateProjectContainer = styled.div``; diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index 01314f2ad..4e42a80cb 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -4,27 +4,16 @@ import { ProjectCard } from "../../../components/ProjectsComponents"; import { CardsGrid, CreateButton, SearchButton, SearchField, OwnProject } from "./styles"; import { useAuth } from "../../../contexts/auth-context"; import { Project } from "../../../data/interfaces"; -import { useNavigate } from "react-router-dom"; -/** - * - * @returns The projects overview page where you can see all the projects. - * You can filter on your own projects or filter on project name. - * - */ function ProjectPage() { const [projectsAPI, setProjectsAPI] = useState>([]); - const [gotProjects, setGotProjects] = useState(false); - - // To filter projects we need to keep a separate list to avoid calling the API every time we change te filters. const [projects, setProjects] = useState>([]); + const [gotProjects, setGotProjects] = useState(false); - // Keep track of the set filters const [searchString, setSearchString] = useState(""); const [ownProjects, setOwnProjects] = useState(false); - const navigate = useNavigate(); - const { userId, role } = useAuth(); + const { userId } = useAuth(); /** * Uses to filter the results based onto search string and own projects @@ -77,11 +66,7 @@ function ProjectPage() { placeholder="project name" /> Search - {role === 0 ? ( - navigate("/editions/summerof2022/projects/new")}> - Create Project - - ) : null} + Create Project
                                      Date: Sat, 16 Apr 2022 19:15:40 +0200 Subject: [PATCH 396/536] typing and some comments --- frontend/src/Router.tsx | 5 ++++- .../views/projectViews/ProjectsPage/ProjectsPage.tsx | 11 ++++++++++- .../src/views/projectViews/ProjectsPage/styles.ts | 8 ++++---- frontend/src/views/projectViews/index.ts | 2 +- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index eec3fd9ef..d1f27f374 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -63,7 +63,10 @@ export default function Router() { } /> {/* TODO project page */} - } /> + } + /> {/* Students routes */} diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index 4e42a80cb..e0703a300 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -5,11 +5,20 @@ import { CardsGrid, CreateButton, SearchButton, SearchField, OwnProject } from " import { useAuth } from "../../../contexts/auth-context"; import { Project } from "../../../data/interfaces"; +/** + * + * @returns The projects overview page where you can see all the projects. + * You can filter on your own projects or filter on project name. + * + */ function ProjectPage() { const [projectsAPI, setProjectsAPI] = useState>([]); - const [projects, setProjects] = useState>([]); const [gotProjects, setGotProjects] = useState(false); + // To filter projects we need to keep a separate list to avoid calling the API every time we change te filters. + const [projects, setProjects] = useState>([]); + + // Keep track of the set filters const [searchString, setSearchString] = useState(""); const [ownProjects, setOwnProjects] = useState(false); diff --git a/frontend/src/views/projectViews/ProjectsPage/styles.ts b/frontend/src/views/projectViews/ProjectsPage/styles.ts index c39d7338f..cccb5f321 100644 --- a/frontend/src/views/projectViews/ProjectsPage/styles.ts +++ b/frontend/src/views/projectViews/ProjectsPage/styles.ts @@ -15,7 +15,7 @@ export const SearchField = styled.input` color: white; border: none; border-radius: 10px; -` +`; export const SearchButton = styled.button` padding: 5px 10px; @@ -23,7 +23,7 @@ export const SearchButton = styled.button` color: white; border: none; border-radius: 10px; -` +`; export const CreateButton = styled.button` margin-left: 25px; @@ -32,8 +32,8 @@ export const CreateButton = styled.button` color: white; border: none; border-radius: 10px; -` +`; export const OwnProject = styled(Form.Check)` margin-top: 10px; margin-left: 20px; -` +`; diff --git a/frontend/src/views/projectViews/index.ts b/frontend/src/views/projectViews/index.ts index 944f237c5..e8615fbac 100644 --- a/frontend/src/views/projectViews/index.ts +++ b/frontend/src/views/projectViews/index.ts @@ -1,2 +1,2 @@ export { default as ProjectsPage } from "./ProjectsPage"; -export { default as ProjectDetailPage } from "./ProjectDetailPage" \ No newline at end of file +export { default as ProjectDetailPage } from "./ProjectDetailPage"; From f740ffac2725dd2879507a03194fa725d58c595d Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sat, 16 Apr 2022 19:30:14 +0200 Subject: [PATCH 397/536] Add missing invites dependency checks --- backend/src/app/routers/editions/invites/invites.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/app/routers/editions/invites/invites.py b/backend/src/app/routers/editions/invites/invites.py index d86269830..80e97d75e 100644 --- a/backend/src/app/routers/editions/invites/invites.py +++ b/backend/src/app/routers/editions/invites/invites.py @@ -32,7 +32,7 @@ async def create_invite(email: EmailAddress, db: Session = Depends(get_session), @invites_router.delete("/{invite_uuid}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, - dependencies=[Depends(require_admin)]) + dependencies=[Depends(require_admin), Depends(get_edition)]) async def delete_invite(invite_link: InviteLinkDB = Depends(get_invite_link), db: Session = Depends(get_session)): """ Delete an existing invitation link manually so that it can't be used anymore. @@ -40,7 +40,7 @@ async def delete_invite(invite_link: InviteLinkDB = Depends(get_invite_link), db delete_invite_link(db, invite_link) -@invites_router.get("/{invite_uuid}", response_model=InviteLinkModel) +@invites_router.get("/{invite_uuid}", response_model=InviteLinkModel, dependencies=[Depends(get_edition)]) async def get_invite(invite_link: InviteLinkDB = Depends(get_invite_link)): """ Get a specific invitation link to see if it exists or not. Can be used to verify the validity From eee276e0ae836ac3164ed32be1a2b123774c13b9 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 16 Apr 2022 19:53:36 +0200 Subject: [PATCH 398/536] More comments for projects page and project detail page --- .../ConfirmDelete/ConfirmDelete.tsx | 10 +++- .../ProjectCard/ProjectCard.tsx | 53 ++++++++++--------- .../StudentPlaceholder/StudentPlaceholder.tsx | 5 ++ frontend/src/data/interfaces/projects.ts | 37 +++++++++++++ .../ProjectDetailPage/ProjectDetailPage.tsx | 3 ++ .../ProjectsPage/ProjectsPage.tsx | 11 +--- 6 files changed, 83 insertions(+), 36 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ConfirmDelete/ConfirmDelete.tsx b/frontend/src/components/ProjectsComponents/ConfirmDelete/ConfirmDelete.tsx index 8c70148db..b727a8a40 100644 --- a/frontend/src/components/ProjectsComponents/ConfirmDelete/ConfirmDelete.tsx +++ b/frontend/src/components/ProjectsComponents/ConfirmDelete/ConfirmDelete.tsx @@ -1,5 +1,13 @@ import { StyledModal, ModalFooter, ModalHeader, Button, DeleteButton } from "./styles"; +/** + * + * @param visible wether to display the confirm screen. + * @param handleClose what to do when the user closes the confirm screen. + * @param handleConfirm what to do when the user confirms the delete action. + * @param name the name of the project that is going to be deleted. + * @returns the modal the confirm the deletion of a project. + */ export default function ConfirmDelete({ visible, handleClose, @@ -7,8 +15,8 @@ export default function ConfirmDelete({ name, }: { visible: boolean; - handleConfirm: () => void; handleClose: () => void; + handleConfirm: () => void; name: string; }) { return ( diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index c869369fc..03d6b6f73 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -22,41 +22,42 @@ import ConfirmDelete from "../ConfirmDelete"; import { deleteProject } from "../../../utils/api/projects"; import { useNavigate } from "react-router-dom"; -import { Coach, Partner } from "../../../data/interfaces"; +import { Project } from "../../../data/interfaces"; +/** + * + * @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({ - name, - partners, - numberOfStudents, - coaches, - edition, - projectId, - refreshEditions, + project, + refreshProjects, }: { - name: string; - partners: Partner[]; - numberOfStudents: number; - coaches: Coach[]; - edition: string; - projectId: number; - refreshEditions: () => void; + project: Project; + refreshProjects: () => void; }) { + // 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(edition, projectId); + deleteProject(project.editionName, project.projectId); setShow(false); - refreshEditions(); + refreshProjects(); }; - const handleShow = () => setShow(true); const navigate = useNavigate(); return ( - navigate("/editions/summerof2022/projects/" + projectId)}> - {name} + <Title + onClick={() => navigate("/editions/summerof2022/projects/" + project.projectId)} + > + {project.name} <OpenIcon /> @@ -68,26 +69,26 @@ export default function ProjectCard({ visible={show} handleConfirm={handleDelete} handleClose={handleClose} - name={name} + name={project.name} > - {partners.map((element, _index) => ( - {element.name} + {project.partners.map((partner, _index) => ( + {partner.name} ))} - {numberOfStudents} + {project.numberOfStudents} - {coaches.map((element, _index) => ( + {project.coaches.map((coach, _index) => ( - {element.name} + {coach.name} ))} diff --git a/frontend/src/components/ProjectsComponents/StudentPlaceholder/StudentPlaceholder.tsx b/frontend/src/components/ProjectsComponents/StudentPlaceholder/StudentPlaceholder.tsx index 2bace00cf..ea2c5bf33 100644 --- a/frontend/src/components/ProjectsComponents/StudentPlaceholder/StudentPlaceholder.tsx +++ b/frontend/src/components/ProjectsComponents/StudentPlaceholder/StudentPlaceholder.tsx @@ -2,6 +2,11 @@ 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 ( diff --git a/frontend/src/data/interfaces/projects.ts b/frontend/src/data/interfaces/projects.ts index b70dc4d54..61b6e8205 100644 --- a/frontend/src/data/interfaces/projects.ts +++ b/frontend/src/data/interfaces/projects.ts @@ -1,23 +1,60 @@ +/** + * This file contains al interfaces used in projects pages. + */ + +/** + * Data about a partner. + */ export interface Partner { + /** The name of the partner */ name: string; } +/** + * Data about a coach. + */ export interface Coach { + /** The name of the coach */ name: string; + + /** The user's ID */ userId: number; } +/** + * Data about a project. + * Such as a list of the partners and the coaches + */ export interface Project { + /** The name of the project */ name: string; + + /** How many students are needed for this project */ numberOfStudents: number; + + /** The partners of this project */ partners: Partner[]; + + /** The coaches of this project */ coaches: Coach[]; + + /** The name of the edition this project belongs to */ editionName: string; + + /** The project's ID */ projectId: number; } +/** + * Data about a place in a project + */ export interface StudentPlace { + /** Whether or not this position is filled in */ available: boolean; + + /** The skill needed to for this place */ skill: string; + + /** The name of the student if this place is filled in */ name: string | undefined; } diff --git a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx index 82394844b..26396f490 100644 --- a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx +++ b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx @@ -23,6 +23,9 @@ import { CoachText, } from "../../../components/ProjectsComponents/ProjectCard/styles"; +/** + * @returns the detailed page of a project. Here you can add or remove students from the project. + */ export default function ProjectDetailPage() { const params = useParams(); const projectId = parseInt(params.projectId!); diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index e0703a300..c39f3ad54 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -6,10 +6,8 @@ import { useAuth } from "../../../contexts/auth-context"; import { Project } from "../../../data/interfaces"; /** - * * @returns The projects overview page where you can see all the projects. * You can filter on your own projects or filter on project name. - * */ function ProjectPage() { const [projectsAPI, setProjectsAPI] = useState>([]); @@ -90,13 +88,8 @@ function ProjectPage() { {projects.map((project, _index) => ( setGotProjects(false)} + project={project} + refreshProjects={() => setGotProjects(false)} key={_index} /> ))} From a9ac824d0c442ac95f609e5c253c11bf628c37ea Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sat, 16 Apr 2022 20:00:40 +0200 Subject: [PATCH 399/536] Fix transaction commit --- backend/src/app/logic/register.py | 23 +++++++++++------------ backend/tests/test_logic/test_register.py | 9 +++++++-- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/backend/src/app/logic/register.py b/backend/src/app/logic/register.py index 6e79af1db..9a48ffa95 100644 --- a/backend/src/app/logic/register.py +++ b/backend/src/app/logic/register.py @@ -13,16 +13,15 @@ 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) - with db.begin_nested() as transaction: - 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) + 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) - transaction.commit() - except sqlalchemy.exc.SQLAlchemyError as exception: - transaction.rollback() - raise FailedToAddNewUserException from exception + db.commit() + except sqlalchemy.exc.SQLAlchemyError as exception: + db.rollback() + raise FailedToAddNewUserException from exception diff --git a/backend/tests/test_logic/test_register.py b/backend/tests/test_logic/test_register.py index 7eb3ff40b..02dea4cd5 100644 --- a/backend/tests/test_logic/test_register.py +++ b/backend/tests/test_logic/test_register.py @@ -44,9 +44,14 @@ def test_duplicate_user(database_session: Session): pw="wachtwoord1", uuid=invite_link1.uuid) nu2 = NewUser(name="user2", email="email@email.com", pw="wachtwoord2", uuid=invite_link2.uuid) - create_request(database_session, nu1, edition) - with pytest.raises(FailedToAddNewUserException): + # 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) + + with pytest.raises(FailedToAddNewUserException), database_session.begin_nested(): create_request(database_session, nu2, edition) # Verify that second user wasn't added From 3023afa6b79ab68b8deb488bce12aca63bed6676 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 16 Apr 2022 20:03:21 +0200 Subject: [PATCH 400/536] prettier --- frontend/src/data/interfaces/index.ts | 2 +- frontend/src/utils/api/projects.ts | 3 +-- frontend/src/views/index.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/src/data/interfaces/index.ts b/frontend/src/data/interfaces/index.ts index b2ffe9df2..27fef25d4 100644 --- a/frontend/src/data/interfaces/index.ts +++ b/frontend/src/data/interfaces/index.ts @@ -1,2 +1,2 @@ export type { User } from "./users"; -export type { Partner, Coach, Project} from "./projects" +export type { Partner, Coach, Project } from "./projects"; diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index 002d3b892..41c91a23a 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -15,7 +15,7 @@ export async function getProjects(edition: string) { } } -export async function getProject(edition:string, projectId: number) { +export async function getProject(edition: string, projectId: number) { try { const response = await axiosInstance.get("/editions/" + edition + "/projects/" + projectId); const project = response.data; @@ -27,7 +27,6 @@ export async function getProject(edition:string, projectId: number) { throw error; } } - } export async function deleteProject(edition: string, projectId: number) { diff --git a/frontend/src/views/index.ts b/frontend/src/views/index.ts index 137777e61..3985a25a3 100644 --- a/frontend/src/views/index.ts +++ b/frontend/src/views/index.ts @@ -2,7 +2,7 @@ export * as Errors from "./errors"; export { default as LoginPage } from "./LoginPage"; export { default as PendingPage } from "./PendingPage"; export { ProjectsPage } from "./projectViews"; -export { ProjectDetailPage } from "./projectViews" +export { ProjectDetailPage } from "./projectViews"; export { default as RegisterPage } from "./RegisterPage"; export { default as StudentsPage } from "./StudentsPage"; export { default as UsersPage } from "./UsersPage"; From 66d44eeaa9cd40c4e6495ce8989815758708f015 Mon Sep 17 00:00:00 2001 From: Tiebe-Vercoutter <99681826+Tiebe-Vercoutter@users.noreply.github.com> Date: Sat, 16 Apr 2022 21:36:32 +0200 Subject: [PATCH 401/536] Apply some suggestions from code review Co-authored-by: Stijn De Clercq <60451863+stijndcl@users.noreply.github.com> --- .../ConfirmDelete/ConfirmDelete.tsx | 2 +- frontend/src/data/interfaces/projects.ts | 4 ++-- frontend/src/utils/api/projects.ts | 11 ++++++----- .../ProjectDetailPage/ProjectDetailPage.tsx | 4 ++-- .../views/projectViews/ProjectsPage/ProjectsPage.tsx | 6 +++--- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ConfirmDelete/ConfirmDelete.tsx b/frontend/src/components/ProjectsComponents/ConfirmDelete/ConfirmDelete.tsx index b727a8a40..1f728104a 100644 --- a/frontend/src/components/ProjectsComponents/ConfirmDelete/ConfirmDelete.tsx +++ b/frontend/src/components/ProjectsComponents/ConfirmDelete/ConfirmDelete.tsx @@ -2,7 +2,7 @@ import { StyledModal, ModalFooter, ModalHeader, Button, DeleteButton } from "./s /** * - * @param visible wether to display the confirm screen. + * @param visible whether to display the confirm screen. * @param handleClose what to do when the user closes the confirm screen. * @param handleConfirm what to do when the user confirms the delete action. * @param name the name of the project that is going to be deleted. diff --git a/frontend/src/data/interfaces/projects.ts b/frontend/src/data/interfaces/projects.ts index 61b6e8205..fd6977ee8 100644 --- a/frontend/src/data/interfaces/projects.ts +++ b/frontend/src/data/interfaces/projects.ts @@ -1,5 +1,5 @@ /** - * This file contains al interfaces used in projects pages. + * This file contains all interfaces used in projects pages. */ /** @@ -52,7 +52,7 @@ export interface StudentPlace { /** Whether or not this position is filled in */ available: boolean; - /** The skill needed to for this place */ + /** The skill needed for this place */ skill: string; /** The name of the student if this place is filled in */ diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index 41c91a23a..a99eacc43 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -8,7 +8,7 @@ export async function getProjects(edition: string) { return projects; } catch (error) { if (axios.isAxiosError(error)) { - return false; + return null; } else { throw error; } @@ -22,19 +22,20 @@ export async function getProject(edition: string, projectId: number) { return project; } catch (error) { if (axios.isAxiosError(error)) { - return false; + return null; } else { throw error; } } } -export async function deleteProject(edition: string, projectId: number) { +export async function deleteProject(edition: string, projectId: number): boolean { try { - const response = await axiosInstance.delete( + await axiosInstance.delete( "/editions/" + edition + "/projects/" + projectId ); - console.log(response); + + return true; } catch (error) { if (axios.isAxiosError(error)) { return false; diff --git a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx index 26396f490..a5a3e1741 100644 --- a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx +++ b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx @@ -35,7 +35,7 @@ export default function ProjectDetailPage() { const navigate = useNavigate(); - const [students, setStudents] = useState>([]); + const [students, setStudents] = useState([]); useEffect(() => { async function callProjects() { @@ -62,7 +62,7 @@ export default function ProjectDetailPage() { if (!gotProject) { callProjects(); } - }); + }, []); if (project) { return ( diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index c39f3ad54..6603c1472 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -10,11 +10,11 @@ import { Project } from "../../../data/interfaces"; * You can filter on your own projects or filter on project name. */ function ProjectPage() { - const [projectsAPI, setProjectsAPI] = useState>([]); + const [projectsAPI, setProjectsAPI] = useState([]); const [gotProjects, setGotProjects] = useState(false); // To filter projects we need to keep a separate list to avoid calling the API every time we change te filters. - const [projects, setProjects] = useState>([]); + const [projects, setProjects] = useState([]); // Keep track of the set filters const [searchString, setSearchString] = useState(""); @@ -62,7 +62,7 @@ function ProjectPage() { if (!gotProjects) { callProjects(); } - }); + }, []); return (
                                      From 17d3f35fc414065f43bfefe6e5a92e9052503e0f Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 16 Apr 2022 22:04:29 +0200 Subject: [PATCH 402/536] If you want more squarer corners you will have to use force. --- frontend/src/Router.tsx | 2 +- .../src/components/LoginComponents/InputFields/styles.ts | 2 +- .../components/ProjectsComponents/ConfirmDelete/styles.ts | 2 +- .../ProjectsComponents/ProjectCard/ProjectCard.tsx | 2 +- .../components/ProjectsComponents/ProjectCard/styles.ts | 8 ++++---- .../ProjectsComponents/StudentPlaceholder/styles.ts | 2 +- .../components/RegisterComponents/InputFields/styles.ts | 2 +- frontend/src/views/RegisterPage/styles.ts | 2 +- .../projectViews/ProjectDetailPage/ProjectDetailPage.tsx | 4 ++-- .../src/views/projectViews/ProjectsPage/ProjectsPage.tsx | 2 +- frontend/src/views/projectViews/ProjectsPage/styles.ts | 6 +++--- 11 files changed, 17 insertions(+), 17 deletions(-) diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 04bbeb53b..de3d0b755 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -9,7 +9,7 @@ import { LoginPage, PendingPage, ProjectsPage, - ProjectDetailPage + ProjectDetailPage, RegisterPage, StudentsPage, UsersPage, diff --git a/frontend/src/components/LoginComponents/InputFields/styles.ts b/frontend/src/components/LoginComponents/InputFields/styles.ts index a0bb3eeba..d6dccb8d1 100644 --- a/frontend/src/components/LoginComponents/InputFields/styles.ts +++ b/frontend/src/components/LoginComponents/InputFields/styles.ts @@ -7,6 +7,6 @@ export const Input = styled.input` margin-bottom: 10px; text-align: center; font-size: 20px; - border-radius: 10px; + border-radius: 5px; border-width: 0; `; diff --git a/frontend/src/components/ProjectsComponents/ConfirmDelete/styles.ts b/frontend/src/components/ProjectsComponents/ConfirmDelete/styles.ts index a2f3e12f7..bca9b22dc 100644 --- a/frontend/src/components/ProjectsComponents/ConfirmDelete/styles.ts +++ b/frontend/src/components/ProjectsComponents/ConfirmDelete/styles.ts @@ -7,7 +7,7 @@ export const StyledModal = styled(Modal)` margin-top: 5%; .modal-content { background-color: #272741; - border-radius: 10px; + border-radius: 5px; border-color: #f14a3b; } `; diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 03d6b6f73..0a792a45a 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -55,7 +55,7 @@ export default function ProjectCard({ navigate("/editions/summerof2022/projects/" + project.projectId)} + onClick={() => navigate("/editions/2022/projects/" + project.projectId)} > {project.name} <OpenIcon /> diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts index 8ec5a8059..12f74f201 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts +++ b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts @@ -4,7 +4,7 @@ import { BsArrowUpRightSquare } from "react-icons/bs"; export const CardContainer = styled.div` border: 2px solid #1a1a36; - border-radius: 20px; + border-radius: 5px; margin: 10px 20px; padding: 20px 20px 20px 20px; background-color: #323252; @@ -64,10 +64,10 @@ export const CoachesContainer = styled.div` export const CoachContainer = styled.div` background-color: #1a1a36; - border-radius: 10px; + border-radius: 5px; margin-right: 10px; text-align: center; - padding: 10px 20px; + padding: 7.5px 15px; width: fit-content; max-width: 20vw; `; @@ -82,7 +82,7 @@ export const Delete = styled.button` background-color: #f14a3b; padding: 5px 5px; border: 0; - border-radius: 5px; + border-radius: 1px; max-height: 30px; margin-left: 5%; display: flex; diff --git a/frontend/src/components/ProjectsComponents/StudentPlaceholder/styles.ts b/frontend/src/components/ProjectsComponents/StudentPlaceholder/styles.ts index b632afb21..e233461c3 100644 --- a/frontend/src/components/ProjectsComponents/StudentPlaceholder/styles.ts +++ b/frontend/src/components/ProjectsComponents/StudentPlaceholder/styles.ts @@ -6,7 +6,7 @@ export const StudentPlaceContainer = styled.div` margin-top: 30px; padding: 20px; background-color: #323252; - border-radius: 20px; + border-radius: 5px; max-width: 50%; display: flex; align-items: center; diff --git a/frontend/src/components/RegisterComponents/InputFields/styles.ts b/frontend/src/components/RegisterComponents/InputFields/styles.ts index a0bb3eeba..d6dccb8d1 100644 --- a/frontend/src/components/RegisterComponents/InputFields/styles.ts +++ b/frontend/src/components/RegisterComponents/InputFields/styles.ts @@ -7,6 +7,6 @@ export const Input = styled.input` margin-bottom: 10px; text-align: center; font-size: 20px; - border-radius: 10px; + border-radius: 5px; border-width: 0; `; diff --git a/frontend/src/views/RegisterPage/styles.ts b/frontend/src/views/RegisterPage/styles.ts index 01f97fd76..6fb8d13a7 100644 --- a/frontend/src/views/RegisterPage/styles.ts +++ b/frontend/src/views/RegisterPage/styles.ts @@ -22,5 +22,5 @@ export const RegisterButton = styled.button` background: var(--osoc_green); color: white; border: none; - border-radius: 10px; + border-radius: 5px; `; diff --git a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx index 26396f490..d9919e833 100644 --- a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx +++ b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx @@ -41,7 +41,7 @@ export default function ProjectDetailPage() { async function callProjects() { if (projectId) { setGotProject(true); - const response = await getProject("summerof2022", projectId); + const response = await getProject("2022", projectId); if (response) { setProject(response); @@ -68,7 +68,7 @@ export default function ProjectDetailPage() { return ( <div> <ProjectContainer> - <GoBack onClick={() => navigate("/editions/summerof2022/projects/")}> + <GoBack onClick={() => navigate("/editions/2022/projects/")}> <BiArrowBack /> Overview </GoBack> diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index c39f3ad54..348f182d1 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -53,7 +53,7 @@ function ProjectPage() { useEffect(() => { async function callProjects() { setGotProjects(true); - const response = await getProjects("summerof2022"); + const response = await getProjects("2022"); if (response) { setProjectsAPI(response.projects); setProjects(response.projects); diff --git a/frontend/src/views/projectViews/ProjectsPage/styles.ts b/frontend/src/views/projectViews/ProjectsPage/styles.ts index cccb5f321..787324608 100644 --- a/frontend/src/views/projectViews/ProjectsPage/styles.ts +++ b/frontend/src/views/projectViews/ProjectsPage/styles.ts @@ -14,7 +14,7 @@ export const SearchField = styled.input` background-color: #131329; color: white; border: none; - border-radius: 10px; + border-radius: 5px; `; export const SearchButton = styled.button` @@ -22,7 +22,7 @@ export const SearchButton = styled.button` background-color: #00bfff; color: white; border: none; - border-radius: 10px; + border-radius: 5px; `; export const CreateButton = styled.button` @@ -31,7 +31,7 @@ export const CreateButton = styled.button` background-color: #44dba4; color: white; border: none; - border-radius: 10px; + border-radius: 5px; `; export const OwnProject = styled(Form.Check)` margin-top: 10px; From 35ea648496a878982fccf7ac16a67e0a2037adb9 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter <tiebe.vercoutter@protonmail.com> Date: Sat, 16 Apr 2022 22:42:26 +0200 Subject: [PATCH 403/536] Fixes most pr comments --- frontend/src/data/interfaces/projects.ts | 8 +++ frontend/src/utils/api/projects.ts | 12 ++-- .../ProjectDetailPage/ProjectDetailPage.tsx | 71 ++++++++++--------- .../ProjectsPage/ProjectsPage.tsx | 18 ++--- 4 files changed, 56 insertions(+), 53 deletions(-) diff --git a/frontend/src/data/interfaces/projects.ts b/frontend/src/data/interfaces/projects.ts index fd6977ee8..42a322b9f 100644 --- a/frontend/src/data/interfaces/projects.ts +++ b/frontend/src/data/interfaces/projects.ts @@ -45,6 +45,14 @@ export interface Project { projectId: number; } +/** + * Used as an response object for multiple projects + */ +export interface Projects { + /** A list of projects */ + projects: Project[]; +} + /** * Data about a place in a project */ diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index a99eacc43..e49016530 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -1,10 +1,11 @@ import axios from "axios"; +import { Projects, Project } from "../../data/interfaces/projects"; import { axiosInstance } from "./api"; export async function getProjects(edition: string) { try { const response = await axiosInstance.get("/editions/" + edition + "/projects/"); - const projects = response.data; + const projects = response.data as Projects; return projects; } catch (error) { if (axios.isAxiosError(error)) { @@ -18,7 +19,7 @@ export async function getProjects(edition: string) { export async function getProject(edition: string, projectId: number) { try { const response = await axiosInstance.get("/editions/" + edition + "/projects/" + projectId); - const project = response.data; + const project = response.data as Project; return project; } catch (error) { if (axios.isAxiosError(error)) { @@ -29,12 +30,9 @@ export async function getProject(edition: string, projectId: number) { } } -export async function deleteProject(edition: string, projectId: number): boolean { +export async function deleteProject(edition: string, projectId: number) { try { - await axiosInstance.delete( - "/editions/" + edition + "/projects/" + projectId - ); - + await axiosInstance.delete("/editions/" + edition + "/projects/" + projectId); return true; } catch (error) { if (axios.isAxiosError(error)) { diff --git a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx index 4ad474886..441c0f680 100644 --- a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx +++ b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx @@ -45,6 +45,7 @@ export default function ProjectDetailPage() { if (response) { setProject(response); + // TODO // Generate student data const studentsTemplate: StudentPlace[] = []; for (let i = 0; i < response.numberOfStudents; i++) { @@ -62,44 +63,44 @@ export default function ProjectDetailPage() { if (!gotProject) { callProjects(); } - }, []); + }, [gotProject, navigate, projectId]); - if (project) { - return ( - <div> - <ProjectContainer> - <GoBack onClick={() => navigate("/editions/2022/projects/")}> - <BiArrowBack /> - Overview - </GoBack> + if (!project) return null; - <Title>{project.name} + return ( +
                                      + + navigate("/editions/2022/projects/")}> + + Overview + - - {project.partners.map((element, _index) => ( - {element.name} - ))} - - {project.numberOfStudents} - - - + {project.name} - - {project.coaches.map((element, _index) => ( - - {element.name} - - ))} - + + {project.partners.map((element, _index) => ( + {element.name} + ))} + + {project.numberOfStudents} + + + -
                                      - {students.map((element: StudentPlace, _index) => ( - - ))} -
                                      -
                                      -
                                      - ); - } else return
                                      ; + + {project.coaches.map((element, _index) => ( + + {element.name} + + ))} + + +
                                      + {students.map((element: StudentPlace, _index) => ( + + ))} +
                                      + +
                                      + ); } diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index 372212704..6758fad04 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -9,7 +9,7 @@ import { Project } from "../../../data/interfaces"; * @returns The projects overview page where you can see all the projects. * You can filter on your own projects or filter on project name. */ -function ProjectPage() { +export default function ProjectPage() { const [projectsAPI, setProjectsAPI] = useState([]); const [gotProjects, setGotProjects] = useState(false); @@ -28,18 +28,16 @@ function ProjectPage() { useEffect(() => { const results: Project[] = []; projectsAPI.forEach(project => { - let ownsProject = true; + let filterOut = false; if (ownProjects) { - ownsProject = false; - project.coaches.forEach(coach => { - if (coach.userId === userId) { - ownsProject = true; - } + // If the user doesn't coach this project it will be filtered out. + filterOut = !project.coaches.some(coach => { + return coach.userId === userId; }); } if ( project.name.toLocaleLowerCase().includes(searchString.toLocaleLowerCase()) && - ownsProject + !filterOut ) { results.push(project); } @@ -62,7 +60,7 @@ function ProjectPage() { if (!gotProjects) { callProjects(); } - }, []); + }, [gotProjects]); return (
                                      @@ -97,5 +95,3 @@ function ProjectPage() {
                                      ); } - -export default ProjectPage; From ffe19ce1d78f7eade95c03726b2ffeab04a22121 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 16 Apr 2022 22:55:53 +0200 Subject: [PATCH 404/536] prettier --- .../components/ProjectsComponents/ProjectCard/ProjectCard.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 0a792a45a..0441a853c 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -54,9 +54,7 @@ export default function ProjectCard({ return ( - navigate("/editions/2022/projects/" + project.projectId)} - > + <Title onClick={() => navigate("/editions/2022/projects/" + project.projectId)}> {project.name} <OpenIcon /> From 0745cd686217fe9b8a032bd52eaa1f89c0954c39 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 16 Apr 2022 23:58:44 +0200 Subject: [PATCH 405/536] Logic to create a new project Some basic page to create a project --- frontend/src/data/interfaces/projects.ts | 20 ++++++ frontend/src/utils/api/projects.ts | 32 +++++++++- .../CreateProjectPage/CreateProjectPage.tsx | 64 +++++++++++++++++-- .../projectViews/CreateProjectPage/styles.ts | 13 +++- 4 files changed, 122 insertions(+), 7 deletions(-) diff --git a/frontend/src/data/interfaces/projects.ts b/frontend/src/data/interfaces/projects.ts index 42a322b9f..ad8569d99 100644 --- a/frontend/src/data/interfaces/projects.ts +++ b/frontend/src/data/interfaces/projects.ts @@ -53,6 +53,26 @@ export interface Projects { projects: Project[]; } +/** + * Used when creating a new project + */ +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[]; + + /** The partners that belong to this project */ + partners: Partner[]; + + /** The users that will coach this project */ + coaches: Coach[]; +} + /** * Data about a place in a project */ diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index e49016530..384b6c046 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import { Projects, Project } from "../../data/interfaces/projects"; +import { Projects, Project, CreateProject, Coach, Partner } from "../../data/interfaces/projects"; import { axiosInstance } from "./api"; export async function getProjects(edition: string) { @@ -30,6 +30,36 @@ export async function getProject(edition: string, projectId: number) { } } +export async function createProject( + edition: string, + name: string, + numberOfStudents: number, + skills: string[], + partners: Partner[], + coaches: Coach[] +) { + const payload: CreateProject = { + name: name, + number_of_students: numberOfStudents, + skills: skills, + partners: partners, + coaches: coaches, + }; + + try { + const response = await axiosInstance.post("editions/" + edition + "/projects/", payload); + const project = response.data as Project; + + return project; + } catch (error) { + if (axios.isAxiosError(error)) { + return null; + } else { + throw error; + } + } +} + export async function deleteProject(edition: string, projectId: number) { try { await axiosInstance.delete("/editions/" + edition + "/projects/" + projectId); diff --git a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx index 74fb95142..8cc1f5726 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx +++ b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx @@ -1,8 +1,62 @@ +import { CreateProjectContainer, Input } from "./styles"; +import { createProject } from "../../../utils/api/projects"; +import { useState } from "react"; +import { GoBack } from "../ProjectDetailPage/styles"; +import { BiArrowBack } from "react-icons/bi"; +import { useNavigate } from "react-router-dom"; + export default function CreateProjectPage() { + const [name, setName] = useState(""); + const [numberOfStudents, setNumberOfStudents] = useState(0); + const [skills, setSkills] = useState([]); + const [partners, setPartners] = useState([]); + const [coaches, setCoaches] = useState([]); + + const navigate = useNavigate(); - return( -
                                      - -
                                      - ) + return ( + + navigate("/editions/2022/projects/")}> + + Cancel + +

                                      New Project

                                      + setName(e.target.value)} + placeholder="Project name" + /> +
                                      + setNumberOfStudents(e.target.valueAsNumber)} + placeholder="Number of students" + /> +
                                      +
                                      + setCoaches([])} placeholder="Coach" /> + +
                                      +
                                      + setSkills([])} placeholder="Skill" /> + +
                                      +
                                      + setPartners([])} + placeholder="Partner" + /> + +
                                      + +
                                      + ); } diff --git a/frontend/src/views/projectViews/CreateProjectPage/styles.ts b/frontend/src/views/projectViews/CreateProjectPage/styles.ts index 8e0438e88..34583e4de 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/styles.ts +++ b/frontend/src/views/projectViews/CreateProjectPage/styles.ts @@ -1,3 +1,14 @@ import styled from "styled-components"; -export const CreateProjectContainer = styled.div``; +export const CreateProjectContainer = styled.div` + margin: 20px; +`; + +export const Input = styled.input` + margin-top: 10px; + padding: 5px 10px; + background-color: #131329; + color: white; + border: none; + border-radius: 5px; +`; From 738f5c73cea40982a0fca3b214c4ba5cf84ebe6f Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 17 Apr 2022 00:26:37 +0200 Subject: [PATCH 406/536] basic adding of coaches using datalist to show options --- frontend/src/data/interfaces/projects.ts | 2 +- frontend/src/utils/api/projects.ts | 4 +-- .../CreateProjectPage/CreateProjectPage.tsx | 35 ++++++++++++++----- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/frontend/src/data/interfaces/projects.ts b/frontend/src/data/interfaces/projects.ts index ad8569d99..e47dac169 100644 --- a/frontend/src/data/interfaces/projects.ts +++ b/frontend/src/data/interfaces/projects.ts @@ -70,7 +70,7 @@ export interface CreateProject { partners: Partner[]; /** The users that will coach this project */ - coaches: Coach[]; + coaches: string[]; } /** diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index 384b6c046..acbb0da4d 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import { Projects, Project, CreateProject, Coach, Partner } from "../../data/interfaces/projects"; +import { Projects, Project, CreateProject, Partner } from "../../data/interfaces/projects"; import { axiosInstance } from "./api"; export async function getProjects(edition: string) { @@ -36,7 +36,7 @@ export async function createProject( numberOfStudents: number, skills: string[], partners: Partner[], - coaches: Coach[] + coaches: string[] ) { const payload: CreateProject = { name: name, diff --git a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx index 8cc1f5726..f256c1ee8 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx +++ b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx @@ -10,7 +10,8 @@ export default function CreateProjectPage() { const [numberOfStudents, setNumberOfStudents] = useState(0); const [skills, setSkills] = useState([]); const [partners, setPartners] = useState([]); - const [coaches, setCoaches] = useState([]); + const [coach, setCoach] = useState(""); + const [coaches, setCoaches] = useState([]); const navigate = useNavigate(); @@ -26,6 +27,7 @@ export default function CreateProjectPage() { onChange={e => setName(e.target.value)} placeholder="Project name" /> +
                                      - setCoaches([])} placeholder="Coach" /> - + setCoach(e.target.value)} + list="users" + placeholder="Coach" + /> + + + +
                                      +
                                      + {coaches.map((element, _index )=> (
                                      {element}
                                      ))}
                                      setSkills([])} placeholder="Skill" />
                                      - setPartners([])} - placeholder="Partner" - /> + setPartners([])} placeholder="Partner" />
                                    - {coaches.map((element, _index )=> (
                                    {element}
                                    ))} + {coaches.map((element, _index) => ( +
                                    + {element} + +
                                    + ))}
                                    setSkills([])} placeholder="Skill" /> From 3bef3bc47f125694a461cccce8a0b73b5d3429d8 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 17 Apr 2022 12:28:11 +0200 Subject: [PATCH 408/536] bootstrap alerts when invalid coach is added --- .../CreateProjectPage/CreateProjectPage.tsx | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx index 9bd818da9..35015a845 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx +++ b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx @@ -4,14 +4,19 @@ import { useState } from "react"; import { GoBack } from "../ProjectDetailPage/styles"; import { BiArrowBack } from "react-icons/bi"; import { useNavigate } from "react-router-dom"; +import Alert from "react-bootstrap/Alert"; export default function CreateProjectPage() { const [name, setName] = useState(""); const [numberOfStudents, setNumberOfStudents] = useState(0); const [skills, setSkills] = useState([]); const [partners, setPartners] = useState([]); + const [coach, setCoach] = useState(""); const [coaches, setCoaches] = useState([]); + const availableCoaches = ["coach1", "coach2", "admin1", "admin2"]; // TODO get users from API call + + const [showAlert, setShowAlert] = useState(false); const navigate = useNavigate(); @@ -45,20 +50,27 @@ export default function CreateProjectPage() { placeholder="Coach" /> - + +
                                    {coaches.map((element, _index) => ( @@ -94,3 +106,20 @@ export default function CreateProjectPage() { ); } + +function AlertDismissibleExample({ + show, + setShow, +}: { + show: boolean; + setShow: (state: boolean) => void; +}) { + if (show) { + return ( + setShow(false)} dismissible> + Please choose an option from the list + + ); + } + return null; +} From 56935546afca9bc25c695e891a332f4012f4cea1 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 17 Apr 2022 15:01:12 +0200 Subject: [PATCH 409/536] style the create project page --- .../CreateProjectPage/CreateProjectPage.tsx | 56 ++++++++++--------- .../projectViews/CreateProjectPage/styles.ts | 43 ++++++++++++++ 2 files changed, 73 insertions(+), 26 deletions(-) diff --git a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx index 35015a845..f7fc1bacb 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx +++ b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx @@ -1,14 +1,23 @@ -import { CreateProjectContainer, Input } from "./styles"; +import { + CreateProjectContainer, + Input, + AddButton, + RemoveButton, + CreateButton, + AddedCoach, + WarningContainer, +} from "./styles"; import { createProject } from "../../../utils/api/projects"; import { useState } from "react"; import { GoBack } from "../ProjectDetailPage/styles"; import { BiArrowBack } from "react-icons/bi"; import { useNavigate } from "react-router-dom"; import Alert from "react-bootstrap/Alert"; +import { TiDeleteOutline } from "react-icons/ti"; export default function CreateProjectPage() { const [name, setName] = useState(""); - const [numberOfStudents, setNumberOfStudents] = useState(0); + const [numberOfStudents, setNumberOfStudents] = useState(0); const [skills, setSkills] = useState([]); const [partners, setPartners] = useState([]); @@ -38,7 +47,9 @@ export default function CreateProjectPage() { type="number" min="0" value={numberOfStudents} - onChange={e => setNumberOfStudents(e.target.valueAsNumber)} + onChange={e => { + setNumberOfStudents(e.target.valueAsNumber); + }} placeholder="Number of students" />
                                    @@ -55,7 +66,7 @@ export default function CreateProjectPage() { })} - - + + + +
                                    {coaches.map((element, _index) => ( -
                                    + {element} - -
                                    + + + ))}
                                    setSkills([])} placeholder="Skill" /> - + Add skill
                                    setPartners([])} placeholder="Partner" /> - + Add partner
                                    - + ); } -function AlertDismissibleExample({ - show, - setShow, -}: { - show: boolean; - setShow: (state: boolean) => void; -}) { +function BadCoachAlert({ show, setShow }: { show: boolean; setShow: (state: boolean) => void }) { if (show) { return ( setShow(false)} dismissible> diff --git a/frontend/src/views/projectViews/CreateProjectPage/styles.ts b/frontend/src/views/projectViews/CreateProjectPage/styles.ts index 34583e4de..9953cbbaa 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/styles.ts +++ b/frontend/src/views/projectViews/CreateProjectPage/styles.ts @@ -12,3 +12,46 @@ export const Input = styled.input` border: none; border-radius: 5px; `; + +export const AddButton = styled.button` + padding: 5px 10px; + background-color: #00bfff; + color: white; + border: none; + margin-left: 5px; + 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 CreateButton = styled.button` + padding: 5px 10px; + background-color: #44dba4; + color: white; + border: none; + margin-top: 10px; + border-radius: 5px; +`; + +export const AddedCoach = styled.div` + margin: 5px; + padding: 5px; + background-color: #1a1a36; + max-width: fit-content; + border-radius: 5px; + display: flex; +`; + +export const WarningContainer = styled.div` + max-width: fit-content; + margin-top: 10px; +`; From 2d148bbc180c8bc558efefcdad7ba7e3ba31ea9b Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 17 Apr 2022 15:44:47 +0200 Subject: [PATCH 410/536] using more separate components --- .../InputFields/Coach/Coach.tsx | 61 +++++++++++++++ .../InputFields/Coach/index.ts | 1 + .../InputFields/Name/Name.tsx | 7 ++ .../InputFields/Name/index.ts | 1 + .../NumberOfStudents/NumberOfStudents.tsx | 23 ++++++ .../InputFields/NumberOfStudents/index.ts | 1 + .../ProjectsComponents/InputFields/index.ts | 3 + .../ProjectsComponents/InputFields/styles.ts | 24 ++++++ .../CreateProjectPage/CreateProjectPage.tsx | 77 ++++--------------- .../projectViews/CreateProjectPage/styles.ts | 5 -- 10 files changed, 137 insertions(+), 66 deletions(-) create mode 100644 frontend/src/components/ProjectsComponents/InputFields/Coach/Coach.tsx create mode 100644 frontend/src/components/ProjectsComponents/InputFields/Coach/index.ts create mode 100644 frontend/src/components/ProjectsComponents/InputFields/Name/Name.tsx create mode 100644 frontend/src/components/ProjectsComponents/InputFields/Name/index.ts create mode 100644 frontend/src/components/ProjectsComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx create mode 100644 frontend/src/components/ProjectsComponents/InputFields/NumberOfStudents/index.ts create mode 100644 frontend/src/components/ProjectsComponents/InputFields/index.ts create mode 100644 frontend/src/components/ProjectsComponents/InputFields/styles.ts diff --git a/frontend/src/components/ProjectsComponents/InputFields/Coach/Coach.tsx b/frontend/src/components/ProjectsComponents/InputFields/Coach/Coach.tsx new file mode 100644 index 000000000..4bae326f5 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/InputFields/Coach/Coach.tsx @@ -0,0 +1,61 @@ +import { useState } from "react"; +import { Alert } from "react-bootstrap"; +import { AddButton, Input, WarningContainer } from "../styles"; + +export default function Coach({ + coach, + setCoach, + coaches, + setCoaches, +}: { + coach: string; + setCoach: (coach: string) => void; + coaches: string[]; + setCoaches: (coaches: string[]) => void; +}) { + const [showAlert, setShowAlert] = useState(false); + const availableCoaches = ["coach1", "coach2", "admin1", "admin2"]; // TODO get users from API call + + return ( +
                                    + setCoach(e.target.value)} + list="users" + placeholder="Coach" + /> + + {availableCoaches.map((availableCoach, _index) => { + return + + { + if (availableCoaches.some(availableCoach => availableCoach === coach)) { + const newCoaches = [...coaches]; + newCoaches.push(coach); + setCoaches(newCoaches); + setShowAlert(false); + } else setShowAlert(true); + }} + > + Add coach + + + + +
                                    + ); +} + +function BadCoachAlert({ show, setShow }: { show: boolean; setShow: (state: boolean) => void }) { + if (show) { + return ( + setShow(false)} dismissible> + Please choose an option from the list + + ); + } + return null; +} diff --git a/frontend/src/components/ProjectsComponents/InputFields/Coach/index.ts b/frontend/src/components/ProjectsComponents/InputFields/Coach/index.ts new file mode 100644 index 000000000..07c25c743 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/InputFields/Coach/index.ts @@ -0,0 +1 @@ +export { default } from "./Coach"; diff --git a/frontend/src/components/ProjectsComponents/InputFields/Name/Name.tsx b/frontend/src/components/ProjectsComponents/InputFields/Name/Name.tsx new file mode 100644 index 000000000..c295f2b98 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/InputFields/Name/Name.tsx @@ -0,0 +1,7 @@ +import { Input } from "../styles" + +export default function Name({ name, setName }: { name: string; setName: (name: string) => void }) { + return ( + setName(e.target.value)} placeholder="Project name" /> + ); +} diff --git a/frontend/src/components/ProjectsComponents/InputFields/Name/index.ts b/frontend/src/components/ProjectsComponents/InputFields/Name/index.ts new file mode 100644 index 000000000..4e90e41d5 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/InputFields/Name/index.ts @@ -0,0 +1 @@ +export { default } from "./Name"; diff --git a/frontend/src/components/ProjectsComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx b/frontend/src/components/ProjectsComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx new file mode 100644 index 000000000..52dfc9999 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx @@ -0,0 +1,23 @@ +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/InputFields/NumberOfStudents/index.ts b/frontend/src/components/ProjectsComponents/InputFields/NumberOfStudents/index.ts new file mode 100644 index 000000000..4eef7d36a --- /dev/null +++ b/frontend/src/components/ProjectsComponents/InputFields/NumberOfStudents/index.ts @@ -0,0 +1 @@ +export {default} from "./NumberOfStudents" \ No newline at end of file diff --git a/frontend/src/components/ProjectsComponents/InputFields/index.ts b/frontend/src/components/ProjectsComponents/InputFields/index.ts new file mode 100644 index 000000000..c34286e21 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/InputFields/index.ts @@ -0,0 +1,3 @@ +export { default as NameInput } from "./Name"; +export { default as NumberOfStudentsInput } from "./NumberOfStudents"; +export { default as CoachInput } from "./Coach"; diff --git a/frontend/src/components/ProjectsComponents/InputFields/styles.ts b/frontend/src/components/ProjectsComponents/InputFields/styles.ts new file mode 100644 index 000000000..8806b3932 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/InputFields/styles.ts @@ -0,0 +1,24 @@ +import styled from "styled-components"; + +export const Input = styled.input` + margin-top: 10px; + padding: 5px 10px; + background-color: #131329; + color: white; + border: none; + border-radius: 5px; +`; + +export const AddButton = styled.button` + padding: 5px 10px; + background-color: #00bfff; + color: white; + border: none; + margin-left: 5px; + border-radius: 5px; +`; + +export const WarningContainer = styled.div` + max-width: fit-content; + margin-top: 10px; +`; diff --git a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx index f7fc1bacb..6154c2d0a 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx +++ b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx @@ -5,15 +5,18 @@ import { RemoveButton, CreateButton, AddedCoach, - WarningContainer, } from "./styles"; import { createProject } from "../../../utils/api/projects"; import { useState } from "react"; +import { useNavigate } from "react-router-dom"; import { GoBack } from "../ProjectDetailPage/styles"; import { BiArrowBack } from "react-icons/bi"; -import { useNavigate } from "react-router-dom"; -import Alert from "react-bootstrap/Alert"; import { TiDeleteOutline } from "react-icons/ti"; +import { + NameInput, + NumberOfStudentsInput, + CoachInput, +} from "../../../components/ProjectsComponents/InputFields"; export default function CreateProjectPage() { const [name, setName] = useState(""); @@ -23,9 +26,6 @@ export default function CreateProjectPage() { const [coach, setCoach] = useState(""); const [coaches, setCoaches] = useState([]); - const availableCoaches = ["coach1", "coach2", "admin1", "admin2"]; // TODO get users from API call - - const [showAlert, setShowAlert] = useState(false); const navigate = useNavigate(); @@ -36,52 +36,18 @@ export default function CreateProjectPage() { Cancel

                                    New Project

                                    - setName(e.target.value)} - placeholder="Project name" + + + -
                                    - { - setNumberOfStudents(e.target.valueAsNumber); - }} - placeholder="Number of students" - /> -
                                    -
                                    - setCoach(e.target.value)} - list="users" - placeholder="Coach" - /> - - {availableCoaches.map((availableCoach, _index) => { - return - - { - if (availableCoaches.some(availableCoach => availableCoach === coach)) { - const newCoaches = [...coaches]; - newCoaches.push(coach); - setCoaches(newCoaches); - setShowAlert(false); - } else setShowAlert(true); - }} - > - Add coach - - - - -
                                    {coaches.map((element, _index) => ( @@ -116,14 +82,3 @@ export default function CreateProjectPage() { ); } - -function BadCoachAlert({ show, setShow }: { show: boolean; setShow: (state: boolean) => void }) { - if (show) { - return ( - setShow(false)} dismissible> - Please choose an option from the list - - ); - } - return null; -} diff --git a/frontend/src/views/projectViews/CreateProjectPage/styles.ts b/frontend/src/views/projectViews/CreateProjectPage/styles.ts index 9953cbbaa..626fd3333 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/styles.ts +++ b/frontend/src/views/projectViews/CreateProjectPage/styles.ts @@ -50,8 +50,3 @@ export const AddedCoach = styled.div` border-radius: 5px; display: flex; `; - -export const WarningContainer = styled.div` - max-width: fit-content; - margin-top: 10px; -`; From 1de820cdd69e9d239fb7da5b0f8f2ce5fc85f2c1 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 17 Apr 2022 16:07:48 +0200 Subject: [PATCH 411/536] more components and better imports --- .../AddedCoaches/AddedCoaches.tsx | 29 ++++++++++++++++ .../AddedCoaches/index.ts | 1 + .../InputFields/Coach/Coach.tsx | 2 +- .../InputFields/Coach/index.ts | 0 .../InputFields/Name/Name.tsx | 2 +- .../InputFields/Name/index.ts | 0 .../NumberOfStudents/NumberOfStudents.tsx | 2 +- .../InputFields/NumberOfStudents/index.ts | 0 .../InputFields/index.ts | 0 .../CreateProjectComponents/index.ts | 2 ++ .../styles.ts | 20 +++++++++++ .../CreateProjectPage/CreateProjectPage.tsx | 34 +++++-------------- .../projectViews/CreateProjectPage/styles.ts | 9 +---- 13 files changed, 64 insertions(+), 37 deletions(-) create mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx create mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/index.ts rename frontend/src/components/ProjectsComponents/{ => CreateProjectComponents}/InputFields/Coach/Coach.tsx (96%) rename frontend/src/components/ProjectsComponents/{ => CreateProjectComponents}/InputFields/Coach/index.ts (100%) rename frontend/src/components/ProjectsComponents/{ => CreateProjectComponents}/InputFields/Name/Name.tsx (85%) rename frontend/src/components/ProjectsComponents/{ => CreateProjectComponents}/InputFields/Name/index.ts (100%) rename frontend/src/components/ProjectsComponents/{ => CreateProjectComponents}/InputFields/NumberOfStudents/NumberOfStudents.tsx (93%) rename frontend/src/components/ProjectsComponents/{ => CreateProjectComponents}/InputFields/NumberOfStudents/index.ts (100%) rename frontend/src/components/ProjectsComponents/{ => CreateProjectComponents}/InputFields/index.ts (100%) create mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts rename frontend/src/components/ProjectsComponents/{InputFields => CreateProjectComponents}/styles.ts (54%) diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx new file mode 100644 index 000000000..4791f30b2 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx @@ -0,0 +1,29 @@ +import { TiDeleteOutline } from "react-icons/ti"; +import { AddedCoach, RemoveButton } from "../styles"; + +export default function AddedCoaches({ + coaches, + setCoaches, +}: { + coaches: string[]; + setCoaches: (coaches: string[]) => void; +}) { + return ( +
                                    + {coaches.map((element, _index) => ( + + {element} + { + const newCoaches = [...coaches]; + newCoaches.splice(_index, 1); + setCoaches(newCoaches); + }} + > + + + + ))} +
                                    + ); +} diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/index.ts new file mode 100644 index 000000000..fe048b5a7 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/index.ts @@ -0,0 +1 @@ +export { default } from "./AddedCoaches"; diff --git a/frontend/src/components/ProjectsComponents/InputFields/Coach/Coach.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx similarity index 96% rename from frontend/src/components/ProjectsComponents/InputFields/Coach/Coach.tsx rename to frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx index 4bae326f5..65dfaa2ad 100644 --- a/frontend/src/components/ProjectsComponents/InputFields/Coach/Coach.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { Alert } from "react-bootstrap"; -import { AddButton, Input, WarningContainer } from "../styles"; +import { AddButton, Input, WarningContainer } from "../../styles"; export default function Coach({ coach, diff --git a/frontend/src/components/ProjectsComponents/InputFields/Coach/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/index.ts similarity index 100% rename from frontend/src/components/ProjectsComponents/InputFields/Coach/index.ts rename to frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/index.ts diff --git a/frontend/src/components/ProjectsComponents/InputFields/Name/Name.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Name/Name.tsx similarity index 85% rename from frontend/src/components/ProjectsComponents/InputFields/Name/Name.tsx rename to frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Name/Name.tsx index c295f2b98..0a74208a8 100644 --- a/frontend/src/components/ProjectsComponents/InputFields/Name/Name.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Name/Name.tsx @@ -1,4 +1,4 @@ -import { Input } from "../styles" +import { Input } from "../../styles" export default function Name({ name, setName }: { name: string; setName: (name: string) => void }) { return ( diff --git a/frontend/src/components/ProjectsComponents/InputFields/Name/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Name/index.ts similarity index 100% rename from frontend/src/components/ProjectsComponents/InputFields/Name/index.ts rename to frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Name/index.ts diff --git a/frontend/src/components/ProjectsComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx similarity index 93% rename from frontend/src/components/ProjectsComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx rename to frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx index 52dfc9999..33279edf6 100644 --- a/frontend/src/components/ProjectsComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx @@ -1,4 +1,4 @@ -import { Input } from "../styles"; +import { Input } from "../../styles"; export default function NumberOfStudents({ numberOfStudents, diff --git a/frontend/src/components/ProjectsComponents/InputFields/NumberOfStudents/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/index.ts similarity index 100% rename from frontend/src/components/ProjectsComponents/InputFields/NumberOfStudents/index.ts rename to frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/index.ts diff --git a/frontend/src/components/ProjectsComponents/InputFields/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts similarity index 100% rename from frontend/src/components/ProjectsComponents/InputFields/index.ts rename to frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts new file mode 100644 index 000000000..643b4a8f3 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts @@ -0,0 +1,2 @@ +export { NameInput, NumberOfStudentsInput, CoachInput } from "./InputFields"; +export { default as AddedCoaches } from "./AddedCoaches"; diff --git a/frontend/src/components/ProjectsComponents/InputFields/styles.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts similarity index 54% rename from frontend/src/components/ProjectsComponents/InputFields/styles.ts rename to frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts index 8806b3932..8d199ae87 100644 --- a/frontend/src/components/ProjectsComponents/InputFields/styles.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts @@ -18,6 +18,26 @@ 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 AddedCoach = styled.div` + margin: 5px; + padding: 5px; + background-color: #1a1a36; + max-width: fit-content; + border-radius: 5px; + display: flex; +`; + export const WarningContainer = styled.div` max-width: fit-content; margin-top: 10px; diff --git a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx index 6154c2d0a..f867ca09a 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx +++ b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx @@ -1,22 +1,15 @@ -import { - CreateProjectContainer, - Input, - AddButton, - RemoveButton, - CreateButton, - AddedCoach, -} from "./styles"; +import { CreateProjectContainer, Input, AddButton, CreateButton } from "./styles"; import { createProject } from "../../../utils/api/projects"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { GoBack } from "../ProjectDetailPage/styles"; import { BiArrowBack } from "react-icons/bi"; -import { TiDeleteOutline } from "react-icons/ti"; import { NameInput, NumberOfStudentsInput, CoachInput, -} from "../../../components/ProjectsComponents/InputFields"; + AddedCoaches, +} from "../../../components/ProjectsComponents/CreateProjectComponents"; export default function CreateProjectPage() { const [name, setName] = useState(""); @@ -24,6 +17,7 @@ export default function CreateProjectPage() { const [skills, setSkills] = useState([]); const [partners, setPartners] = useState([]); + // States for coaches const [coach, setCoach] = useState(""); const [coaches, setCoaches] = useState([]); @@ -36,34 +30,22 @@ export default function CreateProjectPage() { Cancel

                                    New Project

                                    + + + + -
                                    - {coaches.map((element, _index) => ( - - {element} - { - const newCoaches = [...coaches]; - newCoaches.splice(_index, 1); - setCoaches(newCoaches); - }} - > - - - - ))} -
                                    setSkills([])} placeholder="Skill" /> Add skill diff --git a/frontend/src/views/projectViews/CreateProjectPage/styles.ts b/frontend/src/views/projectViews/CreateProjectPage/styles.ts index 626fd3333..dae106a68 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/styles.ts +++ b/frontend/src/views/projectViews/CreateProjectPage/styles.ts @@ -42,11 +42,4 @@ export const CreateButton = styled.button` border-radius: 5px; `; -export const AddedCoach = styled.div` - margin: 5px; - padding: 5px; - background-color: #1a1a36; - max-width: fit-content; - border-radius: 5px; - display: flex; -`; + From d84822fa7e0dbc078949b337d03db5297946f6c5 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 17 Apr 2022 16:59:37 +0200 Subject: [PATCH 412/536] Fix failing test --- .../test_routers/test_editions/test_invites/test_invites.py | 4 ++++ 1 file changed, 4 insertions(+) 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 53e1bcd5d..520c65f77 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 @@ -100,6 +100,10 @@ def test_create_invite_invalid(database_session: Session, auth_client: AuthClien def test_delete_invite_invalid(database_session: Session, auth_client: AuthClient): """Test endpoint for deleting invites when uuid is malformed""" auth_client.admin() + + database_session.add(Edition(year=2022, name="ed2022")) + database_session.commit() + assert auth_client.delete("/editions/ed2022/invites/1").status_code == status.HTTP_422_UNPROCESSABLE_ENTITY From f55adcd122568fee84f23b790c7fb3d720ba3862 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Sun, 17 Apr 2022 17:43:51 +0200 Subject: [PATCH 413/536] migration with email cascade on delete finaly done --- ...24b41f33e_student_has_cascade_on_delete.py | 34 +++++++++++++++++++ backend/src/database/models.py | 4 +-- 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 backend/migrations/versions/94f24b41f33e_student_has_cascade_on_delete.py diff --git a/backend/migrations/versions/94f24b41f33e_student_has_cascade_on_delete.py b/backend/migrations/versions/94f24b41f33e_student_has_cascade_on_delete.py new file mode 100644 index 000000000..a712267d3 --- /dev/null +++ b/backend/migrations/versions/94f24b41f33e_student_has_cascade_on_delete.py @@ -0,0 +1,34 @@ +"""student has cascade on delete + +Revision ID: 94f24b41f33e +Revises: 1862d7dea4cc +Create Date: 2022-04-17 17:41:40.174255 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '94f24b41f33e' +down_revision = '1862d7dea4cc' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('decision_emails', schema=None) as batch_op: + batch_op.drop_constraint('decision_emails_students_fk', type_='foreignkey') + batch_op.create_foreign_key(None, 'students', ['student_id'], ['student_id'], ondelete='CASCADE') + + # ### 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.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key('decision_emails_students_fk', 'students', ['student_id'], ['student_id']) + + # ### end Alembic commands ### diff --git a/backend/src/database/models.py b/backend/src/database/models.py index 68a82f730..490d73a19 100644 --- a/backend/src/database/models.py +++ b/backend/src/database/models.py @@ -76,7 +76,7 @@ class DecisionEmail(Base): __tablename__ = "decision_emails" email_id = Column(Integer, primary_key=True) - student_id = Column(Integer, ForeignKey("students.student_id"), nullable=False) + student_id = Column(Integer, ForeignKey("students.student_id", ondelete="CASCADE"), nullable=False) decision = Column(Enum(DecisionEnum), nullable=False) date = Column(DateTime, nullable=False) @@ -220,7 +220,7 @@ 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") + emails: list[DecisionEmail] = relationship("DecisionEmail", back_populates="student", passive_deletes=True) 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") From 706262fd984b86d20fffddb1abdd40c293c0747d Mon Sep 17 00:00:00 2001 From: FKD13 Date: Sun, 17 Apr 2022 19:50:42 +0200 Subject: [PATCH 414/536] implement refresh tokens --- frontend/src/components/navbar/NavBar.tsx | 6 +- frontend/src/contexts/auth-context.tsx | 42 +---------- frontend/src/data/enums/local-storage.ts | 1 + frontend/src/utils/api/api.ts | 73 ++++++++++++++----- frontend/src/utils/api/auth.ts | 25 +++++-- frontend/src/utils/api/index.ts | 1 - frontend/src/utils/api/login.ts | 12 ++- frontend/src/utils/local-storage/auth.ts | 25 +++++-- .../VerifyingTokenPage/VerifyingTokenPage.tsx | 29 +++++--- 9 files changed, 126 insertions(+), 88 deletions(-) diff --git a/frontend/src/components/navbar/NavBar.tsx b/frontend/src/components/navbar/NavBar.tsx index 202293568..cdbbf34a4 100644 --- a/frontend/src/components/navbar/NavBar.tsx +++ b/frontend/src/components/navbar/NavBar.tsx @@ -8,8 +8,8 @@ import { useAuth } from "../../contexts/auth-context"; * Links are hidden if the user is not authorized to see them. */ export default function NavBar() { - const { accessToken, setAccessToken } = useAuth(); - const hidden = accessToken ? "nav-links" : "nav-hidden"; + const { isLoggedIn } = useAuth(); + const hidden = isLoggedIn ? "nav-links" : "nav-hidden"; return ( <> @@ -32,7 +32,7 @@ export default function NavBar() { { - setAccessToken(null); + localStorage.clear(); }} > Log out diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx index d8e70307d..e3ec5b5d5 100644 --- a/frontend/src/contexts/auth-context.tsx +++ b/frontend/src/contexts/auth-context.tsx @@ -1,11 +1,6 @@ /** Context hook to maintain the authentication state of the user **/ import { Role } from "../data/enums"; -import React, { useContext, ReactNode, useState } from "react"; -import { - getAccessToken, getRefreshToken, - setAccessToken as setAccessTokenInStorage, - setRefreshToken as setRefreshTokenInStorage -} from "../utils/local-storage"; +import React, { ReactNode, useContext, useState } from "react"; /** * Interface that holds the data stored in the AuthContext. @@ -17,10 +12,6 @@ export interface AuthContextState { setRole: (value: Role | null) => void; userId: number | null; setUserId: (value: number | null) => void; - accessToken: string | null; - setAccessToken: (value: string | null) => void; - refreshToken: string | null; - setRefreshToken: (value: string | null) => void; editions: string[]; setEditions: (value: string[]) => void; } @@ -37,11 +28,7 @@ function authDefaultState(): AuthContextState { role: null, setRole: (_: Role | null) => {}, userId: null, - setUserId: (value: number | null) => {}, - accessToken: getAccessToken(), - setAccessToken: (_: string | null) => {}, - refreshToken: getRefreshToken(), - setRefreshToken: (_: string | null) => {}, + setUserId: (_: number | null) => {}, editions: [], setEditions: (_: string[]) => {}, }; @@ -68,9 +55,6 @@ export function AuthProvider({ children }: { children: ReactNode }) { const [role, setRole] = useState(null); const [editions, setEditions] = useState([]); const [userId, setUserId] = useState(null); - // Default value: check LocalStorage - const [accessToken, setAccessToken] = useState(getAccessToken()); - const [refreshToken, setRefreshToken] = useState(getRefreshToken()); // Create AuthContext value const authContextValue: AuthContextState = { @@ -80,28 +64,6 @@ export function AuthProvider({ children }: { children: ReactNode }) { setRole: setRole, userId: userId, setUserId: setUserId, - accessToken: accessToken, - setAccessToken: (value: string | null) => { - // Log the user out if token is null - if (value === null) { - setIsLoggedIn(false); - } - - // Set the token in LocalStorage - setAccessTokenInStorage(value); - setAccessToken(value); - }, - refreshToken: refreshToken, - setRefreshToken: (value: string | null) => { - // Log the user out if token is null - if (value === null) { - setIsLoggedIn(false); - } - - // Set the token in LocalStorage - setRefreshTokenInStorage(value); - setRefreshToken(value); - }, editions: editions, setEditions: setEditions, }; diff --git a/frontend/src/data/enums/local-storage.ts b/frontend/src/data/enums/local-storage.ts index a9a8c5038..a10994f14 100644 --- a/frontend/src/data/enums/local-storage.ts +++ b/frontend/src/data/enums/local-storage.ts @@ -7,4 +7,5 @@ export const enum StorageKey { */ ACCESS_TOKEN = "accessToken", REFRESH_TOKEN = "refreshToken", + REFRESH_TOKEN_LOCK = "refreshTokenLock", } diff --git a/frontend/src/utils/api/api.ts b/frontend/src/utils/api/api.ts index 7374d34ca..abdd0ab36 100644 --- a/frontend/src/utils/api/api.ts +++ b/frontend/src/utils/api/api.ts @@ -1,27 +1,60 @@ -import axios from "axios"; +import axios, { AxiosError } from "axios"; import { BASE_URL } from "../../settings"; +import { + getAccessToken, + getRefreshTokenLock, + setAccessToken, + setRefreshToken, + setRefreshTokenLock, +} from "../local-storage/auth"; +import { refreshTokens } from "./auth"; export const axiosInstance = axios.create(); -axiosInstance.defaults.baseURL = BASE_URL; -axiosInstance.interceptors.response.use(response => { - return response -}, error => { - /* TODO: refresh token */ - return axiosInstance(error.config) -}) +axiosInstance.defaults.baseURL = BASE_URL; -/** - * Function to set the default bearer token in the request headers. - * Passing `null` as the value will remove the header instead. - */ -export function setBearerToken(value: string | null) { - // Remove the header - // Note: setting to "null" or "undefined" is not possible - if (value === null) { - delete axiosInstance.defaults.headers.common.Authorization; - return; +axiosInstance.interceptors.request.use(async config => { + // If the request is sent when a token is being refreshed, delay it for 100ms. + while (getRefreshTokenLock()) { + await new Promise(resolve => setTimeout(resolve, 10)); } + const accessToken = getAccessToken(); + if (accessToken) { + if (config.headers) { + config.headers.Authorization = `Bearer ${accessToken}`; + } + } + return config; +}); + +axiosInstance.interceptors.response.use(undefined, async (error: AxiosError) => { + if (error.response?.status === 401) { + if (getRefreshTokenLock()) { + // If the token is already being refreshed, resend it (will be delayed until the token has been refreshed) + return axiosInstance(error.config); + } else { + setRefreshTokenLock(true); + try { + const tokens = await refreshTokens(); - axiosInstance.defaults.headers.common.Authorization = `Bearer ${value}`; -} + setAccessToken(tokens.access_token); + setRefreshToken(tokens.refresh_token); + + setRefreshTokenLock(false); + + return axiosInstance(error.config); + } catch (refreshError) { + if (axios.isAxiosError(refreshError)) { + const axiosError: AxiosError = refreshError; + if (axiosError.response?.status === 401) { + // refreshing failed with an unauthorized status + localStorage.clear(); + window.location.replace("/"); + } + } + } + setRefreshTokenLock(false); + } + } + throw error; +}); diff --git a/frontend/src/utils/api/auth.ts b/frontend/src/utils/api/auth.ts index 5af463b3b..6ab4f50ad 100644 --- a/frontend/src/utils/api/auth.ts +++ b/frontend/src/utils/api/auth.ts @@ -1,6 +1,7 @@ import axios from "axios"; import { axiosInstance } from "./api"; import { User } from "../../data/interfaces"; +import { getRefreshToken } from "../local-storage"; /** * Check if a bearer token is valid. @@ -19,7 +20,6 @@ export async function validateBearerToken(token: string | null): Promise 404 page als niet ingelogd -* -> swagger werkt weer -* -> async dingen -* */ +export interface Tokens { + access_token: string; + refresh_token: string; +} + +/** + * + */ +export async function refreshTokens(): Promise { + // Don't use axiosInstance to pass interceptors. + const response = await axios.post("/login/refresh", null, { + baseURL: axiosInstance.defaults.baseURL, + headers: { + Authorization: `Bearer ${getRefreshToken()}`, + }, + }); + return response.data as Tokens; +} diff --git a/frontend/src/utils/api/index.ts b/frontend/src/utils/api/index.ts index ef93639c0..c4dfe7af3 100644 --- a/frontend/src/utils/api/index.ts +++ b/frontend/src/utils/api/index.ts @@ -1,2 +1 @@ export { validateRegistrationUrl } from "./auth"; -export { setBearerToken } from "./api"; diff --git a/frontend/src/utils/api/login.ts b/frontend/src/utils/api/login.ts index 16ec6bd23..d1a65eb51 100644 --- a/frontend/src/utils/api/login.ts +++ b/frontend/src/utils/api/login.ts @@ -2,6 +2,7 @@ import axios from "axios"; import { axiosInstance } from "./api"; import { AuthContextState } from "../../contexts"; import { Role } from "../../data/enums"; +import { setAccessToken, setRefreshToken } from "../local-storage"; interface LoginResponse { access_token: string; @@ -20,7 +21,11 @@ interface LoginResponse { * @param email email entered * @param password password entered */ -export async function logIn(auth: AuthContextState, email: string, password: string) { +export async function logIn( + auth: AuthContextState, + email: string, + password: string +): Promise { const payload = new FormData(); payload.append("username", email); payload.append("password", password); @@ -28,8 +33,9 @@ export async function logIn(auth: AuthContextState, email: string, password: str try { const response = await axiosInstance.post("/login/token", payload); const login = response.data as LoginResponse; - auth.setAccessToken(login.access_token); - auth.setRefreshToken(login.refresh_token); + setAccessToken(login.access_token); + setRefreshToken(login.refresh_token); + auth.setIsLoggedIn(true); auth.setRole(login.user.admin ? Role.ADMIN : Role.COACH); auth.setUserId(login.user.userId); diff --git a/frontend/src/utils/local-storage/auth.ts b/frontend/src/utils/local-storage/auth.ts index cfd154b83..96457973a 100644 --- a/frontend/src/utils/local-storage/auth.ts +++ b/frontend/src/utils/local-storage/auth.ts @@ -1,4 +1,4 @@ -import {StorageKey} from "../../data/enums"; +import { StorageKey } from "../../data/enums"; /** * Function to set a new value for the access token in LocalStorage. @@ -14,6 +14,13 @@ export function setRefreshToken(value: string | null) { setToken(StorageKey.REFRESH_TOKEN, value); } +/** + * Function to set a new value for the refresh token lock in LocalStorage. + */ +export function setRefreshTokenLock(value: boolean | null) { + setToken(StorageKey.REFRESH_TOKEN_LOCK, value ? "TRUE" : "FALSE"); +} + function setToken(key: StorageKey, value: string | null) { if (value === null) { localStorage.removeItem(key); @@ -23,19 +30,27 @@ function setToken(key: StorageKey, value: string | null) { } /** - * Function to pull the user's token out of LocalStorage. + * Function to pull the user's access token out of LocalStorage. * Returns `null` if there is no token in LocalStorage yet. */ export function getAccessToken(): string | null { - return getToken(StorageKey.ACCESS_TOKEN) + return getToken(StorageKey.ACCESS_TOKEN); } /** - * Function to pull the user's token out of LocalStorage. + * Function to pull the user's refresh token out of LocalStorage. * Returns `null` if there is no token in LocalStorage yet. */ export function getRefreshToken(): string | null { - return getToken(StorageKey.REFRESH_TOKEN) + return getToken(StorageKey.REFRESH_TOKEN); +} + +/** + * Function to check the refresh token lock in LocalStorage. + * Returns `null` if there is no value in LocalStorage yet. + */ +export function getRefreshTokenLock(): boolean { + return getToken(StorageKey.REFRESH_TOKEN_LOCK) === "TRUE"; } function getToken(key: StorageKey) { diff --git a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx index c95660a53..1d0fd6fc2 100644 --- a/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx +++ b/frontend/src/views/VerifyingTokenPage/VerifyingTokenPage.tsx @@ -1,9 +1,9 @@ import { useEffect } from "react"; -import { setBearerToken } from "../../utils/api"; import { validateBearerToken } from "../../utils/api/auth"; import { Role } from "../../data/enums"; -import { useAuth } from "../../contexts/auth-context"; +import { AuthContextState, useAuth } from "../../contexts/auth-context"; +import { getAccessToken, getRefreshToken } from "../../utils/local-storage"; /** * Placeholder page shown while the bearer token found in LocalStorage is being verified. @@ -14,17 +14,19 @@ export default function VerifyingTokenPage() { useEffect(() => { const verifyToken = async () => { - const response = await validateBearerToken(authContext.accessToken); + const accessToken = getAccessToken(); + const refreshToken = getRefreshToken(); + + if (accessToken === null || refreshToken === null) { + failedVerification(authContext); + return; + } + + const response = await validateBearerToken(accessToken); if (response === null) { - authContext.setAccessToken(null); - authContext.setRefreshToken(null); - authContext.setIsLoggedIn(false); - authContext.setRole(null); - authContext.setEditions([]); + failedVerification(authContext); } else { - // Token was valid, use it as the default request header - setBearerToken(authContext.accessToken); authContext.setIsLoggedIn(true); authContext.setRole(response.admin ? Role.ADMIN : Role.COACH); authContext.setUserId(response.userId); @@ -39,3 +41,10 @@ export default function VerifyingTokenPage() { // This will be replaced later on return

                                    Loading...

                                    ; } + +function failedVerification(authContext: AuthContextState) { + authContext.setIsLoggedIn(false); + authContext.setRole(null); + authContext.setEditions([]); + authContext.setUserId(null); +} From d3d98c4332c5383059fe3c4b45b589d99f4b6299 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Sun, 17 Apr 2022 19:52:58 +0200 Subject: [PATCH 415/536] add pool_pre_ping --- backend/src/database/engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/database/engine.py b/backend/src/database/engine.py index 89aa9a4d1..624332c2b 100644 --- a/backend/src/database/engine.py +++ b/backend/src/database/engine.py @@ -25,6 +25,6 @@ host=settings.DB_HOST, port=settings.DB_PORT, database=settings.DB_NAME - )) + ), pool_pre_ping=True) DBSession = sessionmaker(autocommit=False, autoflush=False, bind=engine) From 7150cdfbb11dc76d0e8651f01c49a367c01d25c5 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Sun, 17 Apr 2022 20:11:03 +0200 Subject: [PATCH 416/536] fix response creation --- backend/src/app/logic/users.py | 8 +-- backend/src/database/crud/users.py | 2 +- .../test_database/test_crud/test_users.py | 64 +++++++++---------- 3 files changed, 35 insertions(+), 39 deletions(-) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index b0547d249..e4856dd6c 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -18,7 +18,7 @@ def get_users_list( and wrap the result in a pydantic model """ - users_orm = users_crud.get_users_filtered(db, admin, edition_name, exclude_edition, name, page) + users_orm = users_crud.get_users_filtered_page(db, admin, edition_name, exclude_edition, name, page) return UsersListResponse(users=[user_model_to_schema(user) for user in users_orm]) @@ -70,11 +70,7 @@ def get_request_list(db: Session, edition_name: str | None, user_name: str | Non else: requests = users_crud.get_requests_for_edition_page(db, edition_name, page, user_name) - requests_model = [] - for request in requests: - user_req = UserRequest(request_id=request.request_id, edition_name=request.edition.name, user=request.user) - requests_model.append(user_req) - return UserRequestsResponse(requests=requests_model) + return UserRequestsResponse(requests=requests) def accept_request(db: Session, request_id: int): diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 5e032d8be..e6080dd70 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -23,7 +23,7 @@ def get_user_edition_names(db: Session, user: User) -> list[str]: return editions -def get_users_filtered( +def get_users_filtered_page( db: Session, admin: bool | None = None, edition_name: str | None = None, diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index 2c0631cdc..d19c5e6dd 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -50,7 +50,7 @@ def test_get_all_users(database_session: Session, data: dict[str, int]): """Test get request for users""" # get all users - users = users_crud.get_users_filtered(database_session) + users = users_crud.get_users_filtered_page(database_session) assert len(users) == 2, "Wrong length" user_ids = [user.user_id for user in users] assert data["user1"] in user_ids @@ -62,8 +62,8 @@ def test_get_all_users_paginated(database_session: Session): database_session.add(models.User(name=f"User {i}", admin=False)) database_session.commit() - assert len(users_crud.get_users_filtered(database_session, page=0)) == DB_PAGE_SIZE - assert len(users_crud.get_users_filtered(database_session, page=1)) == round( + assert len(users_crud.get_users_filtered_page(database_session, page=0)) == DB_PAGE_SIZE + assert len(users_crud.get_users_filtered_page(database_session, page=1)) == round( DB_PAGE_SIZE * 1.5 ) - DB_PAGE_SIZE @@ -76,8 +76,8 @@ def test_get_all_users_paginated_filter_name(database_session: Session): count += 1 database_session.commit() - assert len(users_crud.get_users_filtered(database_session, page=0, name="1")) == count - assert len(users_crud.get_users_filtered(database_session, page=1, name="1")) == max(count - round( + assert len(users_crud.get_users_filtered_page(database_session, page=0, name="1")) == count + assert len(users_crud.get_users_filtered_page(database_session, page=1, name="1")) == max(count - round( DB_PAGE_SIZE * 1.5), 0) @@ -85,7 +85,7 @@ def test_get_all_admins(database_session: Session, data: dict[str, str]): """Test get request for admins""" # get all admins - users = users_crud.get_users_filtered(database_session, admin=True) + users = users_crud.get_users_filtered_page(database_session, admin=True) assert len(users) == 1, "Wrong length" assert data["user1"] == users[0].user_id @@ -100,12 +100,12 @@ def test_get_all_admins_paginated(database_session: Session): database_session.commit() count = len(admins) - users = users_crud.get_users_filtered(database_session, page=0, admin=True) + users = users_crud.get_users_filtered_page(database_session, 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(database_session, page=1, admin=True)) == \ + assert len(users_crud.get_users_filtered_page(database_session, page=1, admin=True)) == \ min(count - DB_PAGE_SIZE, DB_PAGE_SIZE) @@ -119,12 +119,12 @@ def test_get_all_non_admins_paginated(database_session: Session): database_session.commit() count = len(non_admins) - users = users_crud.get_users_filtered(database_session, page=0, admin=False) + users = users_crud.get_users_filtered_page(database_session, 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(database_session, page=1, admin=False)) == \ + assert len(users_crud.get_users_filtered_page(database_session, page=1, admin=False)) == \ min(count - DB_PAGE_SIZE, DB_PAGE_SIZE) @@ -136,8 +136,8 @@ def test_get_all_admins_paginated_filter_name(database_session: Session): count += 1 database_session.commit() - assert len(users_crud.get_users_filtered(database_session, page=0, name="1", admin=True)) == count - assert len(users_crud.get_users_filtered(database_session, page=1, name="1", admin=True)) == max(count - round( + assert len(users_crud.get_users_filtered_page(database_session, page=0, name="1", admin=True)) == count + assert len(users_crud.get_users_filtered_page(database_session, page=1, name="1", admin=True)) == max(count - round( DB_PAGE_SIZE * 1.5), 0) @@ -193,13 +193,13 @@ def test_get_all_users_from_edition(database_session: Session, data: dict[str, s """Test get request for users of a given edition""" # get all users from edition - users = users_crud.get_users_filtered(database_session, edition_name=data["edition1"]) + users = users_crud.get_users_filtered_page(database_session, edition_name=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(database_session, edition_name=data["edition2"]) + users = users_crud.get_users_filtered_page(database_session, edition_name=data["edition2"]) assert len(users) == 1, "Wrong length" assert data["user2"] == users[0].user_id @@ -223,12 +223,12 @@ def test_get_all_users_for_edition_paginated(database_session: Session): ]) database_session.commit() - assert len(users_crud.get_users_filtered(database_session, edition_name=edition_1.name, page=0)) == DB_PAGE_SIZE - assert len(users_crud.get_users_filtered(database_session, edition_name=edition_1.name, page=1)) == round( + assert len(users_crud.get_users_filtered_page(database_session, edition_name=edition_1.name, page=0)) == DB_PAGE_SIZE + assert len(users_crud.get_users_filtered_page(database_session, edition_name=edition_1.name, page=1)) == round( DB_PAGE_SIZE * 1.5 ) - DB_PAGE_SIZE - assert len(users_crud.get_users_filtered(database_session, edition_name=edition_2.name, page=0)) == DB_PAGE_SIZE - assert len(users_crud.get_users_filtered(database_session, edition_name=edition_2.name, page=1)) == round( + assert len(users_crud.get_users_filtered_page(database_session, edition_name=edition_2.name, page=0)) == DB_PAGE_SIZE + assert len(users_crud.get_users_filtered_page(database_session, edition_name=edition_2.name, page=1)) == round( DB_PAGE_SIZE * 1.5 ) - DB_PAGE_SIZE @@ -255,13 +255,13 @@ def test_get_all_users_for_edition_paginated_filter_name(database_session: Sessi count += 1 database_session.commit() - assert len(users_crud.get_users_filtered(database_session, edition_name=edition_1.name, page=0, name="1")) == \ + assert len(users_crud.get_users_filtered_page(database_session, edition_name=edition_1.name, page=0, name="1")) == \ min(count, DB_PAGE_SIZE) - assert len(users_crud.get_users_filtered(database_session, edition_name=edition_1.name, page=1, name="1")) == \ + assert len(users_crud.get_users_filtered_page(database_session, edition_name=edition_1.name, page=1, name="1")) == \ max(count - DB_PAGE_SIZE, 0) - assert len(users_crud.get_users_filtered(database_session, edition_name=edition_2.name, page=0, name="1")) == \ + assert len(users_crud.get_users_filtered_page(database_session, edition_name=edition_2.name, page=0, name="1")) == \ min(count, DB_PAGE_SIZE) - assert len(users_crud.get_users_filtered(database_session, edition_name=edition_2.name, page=1, name="1")) == \ + assert len(users_crud.get_users_filtered_page(database_session, edition_name=edition_2.name, page=1, name="1")) == \ max(count - DB_PAGE_SIZE, 0) @@ -284,18 +284,18 @@ def test_get_all_users_excluded_edition_paginated(database_session: Session): ]) database_session.commit() - a_users = users_crud.get_users_filtered(database_session, page=0, exclude_edition_name="edB", name="") + a_users = users_crud.get_users_filtered_page(database_session, page=0, exclude_edition_name="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(database_session, page=1, exclude_edition_name="edB", name="")) == \ + assert len(users_crud.get_users_filtered_page(database_session, page=1, exclude_edition_name="edB", name="")) == \ round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE - b_users = users_crud.get_users_filtered(database_session, page=0, exclude_edition_name="edA", name="") + b_users = users_crud.get_users_filtered_page(database_session, page=0, exclude_edition_name="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(database_session, page=1, exclude_edition_name="edA", name="")) == \ + assert len(users_crud.get_users_filtered_page(database_session, page=1, exclude_edition_name="edA", name="")) == \ round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE @@ -321,18 +321,18 @@ def test_get_all_users_excluded_edition_paginated_filter_name(database_session: count += 1 database_session.commit() - a_users = users_crud.get_users_filtered(database_session, page=0, exclude_edition_name="edB", name="1") + a_users = users_crud.get_users_filtered_page(database_session, page=0, exclude_edition_name="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(database_session, page=1, exclude_edition_name="edB", name="1")) == \ + assert len(users_crud.get_users_filtered_page(database_session, page=1, exclude_edition_name="edB", name="1")) == \ max(count - DB_PAGE_SIZE, 0) - b_users = users_crud.get_users_filtered(database_session, page=0, exclude_edition_name="edA", name="1") + b_users = users_crud.get_users_filtered_page(database_session, page=0, exclude_edition_name="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(database_session, page=1, exclude_edition_name="edA", name="1")) == \ + assert len(users_crud.get_users_filtered_page(database_session, page=1, exclude_edition_name="edA", name="1")) == \ max(count - DB_PAGE_SIZE, 0) @@ -363,8 +363,8 @@ def test_get_all_users_for_edition_excluded_edition_paginated(database_session: database_session.commit() - users = users_crud.get_users_filtered(database_session, page=0, exclude_edition_name="edB", - edition_name="edA") + users = users_crud.get_users_filtered_page(database_session, page=0, exclude_edition_name="edB", + edition_name="edA") assert len(users) == len(correct_users) for user in users: assert user in correct_users From 917d3de0b9e0f9e3bcd7cbbd7dd245c07224ebdb Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Sun, 17 Apr 2022 20:29:51 +0200 Subject: [PATCH 417/536] removed faulty migrations --- ...24b41f33e_student_has_cascade_on_delete.py | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 backend/migrations/versions/94f24b41f33e_student_has_cascade_on_delete.py diff --git a/backend/migrations/versions/94f24b41f33e_student_has_cascade_on_delete.py b/backend/migrations/versions/94f24b41f33e_student_has_cascade_on_delete.py deleted file mode 100644 index a712267d3..000000000 --- a/backend/migrations/versions/94f24b41f33e_student_has_cascade_on_delete.py +++ /dev/null @@ -1,34 +0,0 @@ -"""student has cascade on delete - -Revision ID: 94f24b41f33e -Revises: 1862d7dea4cc -Create Date: 2022-04-17 17:41:40.174255 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '94f24b41f33e' -down_revision = '1862d7dea4cc' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('decision_emails', schema=None) as batch_op: - batch_op.drop_constraint('decision_emails_students_fk', type_='foreignkey') - batch_op.create_foreign_key(None, 'students', ['student_id'], ['student_id'], ondelete='CASCADE') - - # ### 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.drop_constraint(None, type_='foreignkey') - batch_op.create_foreign_key('decision_emails_students_fk', 'students', ['student_id'], ['student_id']) - - # ### end Alembic commands ### From ea3c43c72910153de396c893e49cb877f483f640 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Sun, 17 Apr 2022 20:38:32 +0200 Subject: [PATCH 418/536] delete faulty migration --- backend/src/database/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/database/models.py b/backend/src/database/models.py index 490d73a19..68a82f730 100644 --- a/backend/src/database/models.py +++ b/backend/src/database/models.py @@ -76,7 +76,7 @@ class DecisionEmail(Base): __tablename__ = "decision_emails" email_id = Column(Integer, primary_key=True) - student_id = Column(Integer, ForeignKey("students.student_id", ondelete="CASCADE"), nullable=False) + student_id = Column(Integer, ForeignKey("students.student_id"), nullable=False) decision = Column(Enum(DecisionEnum), nullable=False) date = Column(DateTime, nullable=False) @@ -220,7 +220,7 @@ 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", passive_deletes=True) + 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") From d507ec4ae79e8d519a37b2dc8091277ae1e3539b Mon Sep 17 00:00:00 2001 From: FKD13 Date: Sun, 17 Apr 2022 22:09:26 +0200 Subject: [PATCH 419/536] move parameters to object --- backend/src/app/logic/users.py | 11 +-- backend/src/app/routers/users/users.py | 11 +-- backend/src/app/schemas/users.py | 11 ++- backend/src/database/crud/users.py | 48 +++++----- .../test_database/test_crud/test_users.py | 93 ++++++++++++------- 5 files changed, 96 insertions(+), 78 deletions(-) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index e4856dd6c..1fdc69c3a 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -1,24 +1,21 @@ from sqlalchemy.orm import Session import src.database.crud.users as users_crud -from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, user_model_to_schema +from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, user_model_to_schema, \ + FilterParameters from src.database.models import User def get_users_list( db: Session, - admin: bool | None, - edition_name: str | None, - exclude_edition: str | None, - name: str | None, - page: int + params: FilterParameters ) -> UsersListResponse: """ Query the database for a list of users and wrap the result in a pydantic model """ - users_orm = users_crud.get_users_filtered_page(db, admin, edition_name, exclude_edition, name, page) + users_orm = users_crud.get_users_filtered_page(db, params) return UsersListResponse(users=[user_model_to_schema(user) for user in users_orm]) diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index 9d9b2fe17..b82f1a98f 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -4,7 +4,8 @@ import src.app.logic.users as logic from src.app.routers.tags import Tags from src.app.schemas.login import UserData -from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, user_model_to_schema +from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, user_model_to_schema, \ + FilterParameters from src.app.utils.dependencies import require_admin, get_current_active_user from src.database.database import get_session from src.database.models import User as UserDB @@ -14,11 +15,7 @@ @users_router.get("/", response_model=UsersListResponse, dependencies=[Depends(require_admin)]) async def get_users( - admin: bool = Query(None), - edition: str | None = Query(None), - exclude_edition: str | None = Query(None), - name: str | None = Query(None), - page: int = 0, + params: FilterParameters = Depends(), db: Session = Depends(get_session)): """ Get users @@ -26,7 +23,7 @@ async def 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, admin, edition, exclude_edition, name, page) + return logic.get_users_list(db, params) @users_router.get("/current", response_model=UserData) diff --git a/backend/src/app/schemas/users.py b/backend/src/app/schemas/users.py index de8fde5c5..27f8cabb1 100644 --- a/backend/src/app/schemas/users.py +++ b/backend/src/app/schemas/users.py @@ -1,5 +1,5 @@ from src.app.schemas.editions import Edition -from src.app.schemas.utils import CamelCaseModel +from src.app.schemas.utils import CamelCaseModel, BaseModel from src.database.models import User as ModelUser @@ -67,3 +67,12 @@ class UserRequestsResponse(CamelCaseModel): """Model for a list of userrequests""" requests: list[UserRequest] + + +class FilterParameters(BaseModel): + """Schema for query parameters""" + edition: str | None + exclude_edition: str | None + name: str | None + admin: bool | None + page: int = 0 diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index e6080dd70..e72bb99bf 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -1,5 +1,6 @@ from sqlalchemy.orm import Session, Query +from src.app.schemas.users import FilterParameters from src.database.crud.editions import get_edition_by_name from src.database.crud.editions import get_editions from src.database.crud.util import paginate @@ -23,14 +24,7 @@ def get_user_edition_names(db: Session, user: User) -> list[str]: return editions -def get_users_filtered_page( - db: Session, - admin: bool | None = None, - edition_name: str | None = None, - exclude_edition_name: str | None = None, - name: str | None = None, - page: int = 0 -): +def get_users_filtered_page(db: Session, params: FilterParameters): """ Get users and filter by optional parameters: :param admin: only return admins / only return non-admins @@ -44,31 +38,31 @@ def get_users_filtered_page( query = db.query(User) - if name is not None: - query = query.where(User.name.contains(name)) + if params.name is not None: + query = query.where(User.name.contains(params.name)) - if admin is not None: - query = query.filter(User.admin.is_(admin)) + 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, page).all() + return paginate(query, params.page).all() - if edition_name is not None: - edition = get_edition_by_name(db, edition_name) + if params.edition is not None: + edition = get_edition_by_name(db, params.edition) query = query \ .join(user_editions) \ .filter(user_editions.c.edition_id == edition.edition_id) - if exclude_edition_name is not None: - exclude_edition = get_edition_by_name(db, exclude_edition_name) + if params.exclude_edition is not None: + exclude_edition = get_edition_by_name(db, params.exclude_edition) 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) - ) + User.user_id.not_in( + db.query(user_editions.c.user_id).where(user_editions.c.edition_id == exclude_edition.edition_id) ) + ) - return paginate(query, page).all() + return paginate(query, params.page).all() def edit_admin_status(db: Session, user_id: int, admin: bool): @@ -130,12 +124,12 @@ def get_requests_page(db: Session, page: int, user_name: str = "") -> list[Coach def _get_requests_for_edition_query(db: Session, edition: Edition, user_name: str = "") -> Query: - return db.query(CoachRequest)\ - .where(CoachRequest.edition_id == edition.edition_id)\ - .join(User)\ - .where(User.name.contains(user_name))\ - .join(AuthEmail, isouter=True)\ - .join(AuthGitHub, isouter=True)\ + return db.query(CoachRequest) \ + .where(CoachRequest.edition_id == edition.edition_id) \ + .join(User) \ + .where(User.name.contains(user_name)) \ + .join(AuthEmail, isouter=True) \ + .join(AuthGitHub, isouter=True) \ .join(AuthGoogle, isouter=True) diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index d19c5e6dd..dbea65b40 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -3,6 +3,7 @@ import src.database.crud.users as users_crud from settings import DB_PAGE_SIZE +from src.app.schemas.users import FilterParameters from src.database import models from src.database.models import user_editions, CoachRequest @@ -50,7 +51,7 @@ def test_get_all_users(database_session: Session, data: dict[str, int]): """Test get request for users""" # get all users - users = users_crud.get_users_filtered_page(database_session) + users = 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 @@ -62,8 +63,8 @@ def test_get_all_users_paginated(database_session: Session): database_session.add(models.User(name=f"User {i}", admin=False)) database_session.commit() - assert len(users_crud.get_users_filtered_page(database_session, page=0)) == DB_PAGE_SIZE - assert len(users_crud.get_users_filtered_page(database_session, page=1)) == round( + 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( DB_PAGE_SIZE * 1.5 ) - DB_PAGE_SIZE @@ -76,16 +77,17 @@ def test_get_all_users_paginated_filter_name(database_session: Session): count += 1 database_session.commit() - assert len(users_crud.get_users_filtered_page(database_session, page=0, name="1")) == count - assert len(users_crud.get_users_filtered_page(database_session, page=1, name="1")) == max(count - round( - DB_PAGE_SIZE * 1.5), 0) + 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( + count - round( + DB_PAGE_SIZE * 1.5), 0) def test_get_all_admins(database_session: Session, data: dict[str, str]): """Test get request for admins""" # get all admins - users = users_crud.get_users_filtered_page(database_session, admin=True) + users = users_crud.get_users_filtered_page(database_session, FilterParameters(admin=True)) assert len(users) == 1, "Wrong length" assert data["user1"] == users[0].user_id @@ -100,12 +102,12 @@ def test_get_all_admins_paginated(database_session: Session): database_session.commit() count = len(admins) - users = users_crud.get_users_filtered_page(database_session, page=0, admin=True) + users = 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, page=1, admin=True)) == \ + assert len(users_crud.get_users_filtered_page(database_session, FilterParameters(page=1, admin=True))) == \ min(count - DB_PAGE_SIZE, DB_PAGE_SIZE) @@ -119,12 +121,12 @@ def test_get_all_non_admins_paginated(database_session: Session): database_session.commit() count = len(non_admins) - users = users_crud.get_users_filtered_page(database_session, page=0, admin=False) + users = 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, page=1, admin=False)) == \ + assert len(users_crud.get_users_filtered_page(database_session, FilterParameters(page=1, admin=False))) == \ min(count - DB_PAGE_SIZE, DB_PAGE_SIZE) @@ -136,9 +138,12 @@ def test_get_all_admins_paginated_filter_name(database_session: Session): count += 1 database_session.commit() - assert len(users_crud.get_users_filtered_page(database_session, page=0, name="1", admin=True)) == count - assert len(users_crud.get_users_filtered_page(database_session, page=1, name="1", admin=True)) == max(count - round( - DB_PAGE_SIZE * 1.5), 0) + assert len( + 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( + count - round( + DB_PAGE_SIZE * 1.5), 0) def test_get_user_edition_names_empty(database_session: Session): @@ -193,13 +198,13 @@ def test_get_all_users_from_edition(database_session: Session, data: dict[str, s """Test get request for users of a given edition""" # get all users from edition - users = users_crud.get_users_filtered_page(database_session, edition_name=data["edition1"]) + users = 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, edition_name=data["edition2"]) + users = 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 @@ -223,12 +228,16 @@ def test_get_all_users_for_edition_paginated(database_session: Session): ]) database_session.commit() - assert len(users_crud.get_users_filtered_page(database_session, edition_name=edition_1.name, page=0)) == DB_PAGE_SIZE - assert len(users_crud.get_users_filtered_page(database_session, edition_name=edition_1.name, page=1)) == round( + 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( DB_PAGE_SIZE * 1.5 ) - DB_PAGE_SIZE - assert len(users_crud.get_users_filtered_page(database_session, edition_name=edition_2.name, page=0)) == DB_PAGE_SIZE - assert len(users_crud.get_users_filtered_page(database_session, edition_name=edition_2.name, page=1)) == round( + 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( DB_PAGE_SIZE * 1.5 ) - DB_PAGE_SIZE @@ -255,13 +264,17 @@ def test_get_all_users_for_edition_paginated_filter_name(database_session: Sessi count += 1 database_session.commit() - assert len(users_crud.get_users_filtered_page(database_session, edition_name=edition_1.name, page=0, name="1")) == \ + assert len(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, edition_name=edition_1.name, page=1, name="1")) == \ + assert len(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, edition_name=edition_2.name, page=0, name="1")) == \ + assert len(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, edition_name=edition_2.name, page=1, name="1")) == \ + assert len(users_crud.get_users_filtered_page(database_session, + FilterParameters(edition=edition_2.name, page=1, name="1"))) == \ max(count - DB_PAGE_SIZE, 0) @@ -284,18 +297,22 @@ def test_get_all_users_excluded_edition_paginated(database_session: Session): ]) database_session.commit() - a_users = users_crud.get_users_filtered_page(database_session, page=0, exclude_edition_name="edB", name="") + a_users = 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, page=1, exclude_edition_name="edB", name="")) == \ + assert len(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, page=0, exclude_edition_name="edA", name="") + b_users = 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, page=1, exclude_edition_name="edA", name="")) == \ + assert len(users_crud.get_users_filtered_page(database_session, + FilterParameters(page=1, exclude_edition="edA", name=""))) == \ round(DB_PAGE_SIZE * 1.5) - DB_PAGE_SIZE @@ -321,18 +338,22 @@ def test_get_all_users_excluded_edition_paginated_filter_name(database_session: count += 1 database_session.commit() - a_users = users_crud.get_users_filtered_page(database_session, page=0, exclude_edition_name="edB", name="1") + a_users = 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, page=1, exclude_edition_name="edB", name="1")) == \ + assert len(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, page=0, exclude_edition_name="edA", name="1") + b_users = 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, page=1, exclude_edition_name="edA", name="1")) == \ + assert len(users_crud.get_users_filtered_page(database_session, + FilterParameters(page=1, exclude_edition="edA", name="1"))) == \ max(count - DB_PAGE_SIZE, 0) @@ -363,8 +384,8 @@ def test_get_all_users_for_edition_excluded_edition_paginated(database_session: database_session.commit() - users = users_crud.get_users_filtered_page(database_session, page=0, exclude_edition_name="edB", - edition_name="edA") + users = 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 @@ -525,7 +546,7 @@ def test_get_requests_paginated_filter_user_name(database_session: Session): assert len(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")) == \ - max(count-DB_PAGE_SIZE, 0) + max(count - DB_PAGE_SIZE, 0) def test_get_all_requests_from_edition(database_session: Session): @@ -594,7 +615,7 @@ def test_get_requests_for_edition_paginated_filter_user_name(database_session: S assert len(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")) == \ - max(count-DB_PAGE_SIZE, 0) + max(count - DB_PAGE_SIZE, 0) def test_accept_request(database_session: Session): From 80102233d8b17ace636d1aa2e30f7d07d14fa68c Mon Sep 17 00:00:00 2001 From: FKD13 Date: Sun, 17 Apr 2022 22:54:29 +0200 Subject: [PATCH 420/536] dont refresh page when using the navbar --- frontend/src/components/Navbar/Navbar.tsx | 13 ++++++++++--- frontend/src/components/Navbar/UsersDropdown.tsx | 11 +++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/Navbar/Navbar.tsx b/frontend/src/components/Navbar/Navbar.tsx index 2b72ba60d..eb81a5117 100644 --- a/frontend/src/components/Navbar/Navbar.tsx +++ b/frontend/src/components/Navbar/Navbar.tsx @@ -9,6 +9,7 @@ import LogoutButton from "./LogoutButton"; import { getCurrentEdition, setCurrentEdition } from "../../utils/session-storage/current-edition"; import { matchPath, useLocation } from "react-router-dom"; import UsersDropdown from "./UsersDropdown"; +import { LinkContainer } from "react-router-bootstrap"; /** * Navbar component displayed at the top of the screen. @@ -62,9 +63,15 @@ export default function Navbar() { diff --git a/frontend/src/components/Navbar/UsersDropdown.tsx b/frontend/src/components/Navbar/UsersDropdown.tsx index e4a9b4ece..cd6ea3a3c 100644 --- a/frontend/src/components/Navbar/UsersDropdown.tsx +++ b/frontend/src/components/Navbar/UsersDropdown.tsx @@ -2,6 +2,7 @@ import { useAuth } from "../../contexts"; import NavDropdown from "react-bootstrap/NavDropdown"; import { StyledDropdownItem } from "./styles"; import { Role } from "../../data/enums"; +import { LinkContainer } from "react-router-bootstrap"; interface Props { currentEdition: string; @@ -22,10 +23,12 @@ export default function UsersDropdown({ currentEdition }: Props) { return ( - Admins - - Coaches - + + Admins + + + Coaches + ); } From e2514f45a160af21e31e9d7ec5f4703ff6665d71 Mon Sep 17 00:00:00 2001 From: Francis <44001949+FKD13@users.noreply.github.com> Date: Sun, 17 Apr 2022 23:53:25 +0200 Subject: [PATCH 421/536] Update students_suggestions.py --- .../editions/students/suggestions/students_suggestions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/routers/editions/students/suggestions/students_suggestions.py b/backend/src/app/routers/editions/students/suggestions/students_suggestions.py index b842fe3fb..8814b93e3 100644 --- a/backend/src/app/routers/editions/students/suggestions/students_suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/students_suggestions.py @@ -13,7 +13,7 @@ async def create_suggestion(edition_id: int, student_id: int): """ -@students_suggestions_router.delete("/{suggestion_id}") +@students_suggestions_router.get("/{suggestion_id}") async def delete_suggestion(edition_id: int, student_id: int, suggestion_id: int): """ Delete a suggestion you made about a student. From 63e1f726a92cb84bff434ac9218f643c4463a6d3 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 18 Apr 2022 12:04:15 +0200 Subject: [PATCH 422/536] Remove pending requests when manually added by an admin --- backend/src/app/logic/users.py | 1 + backend/src/database/crud/users.py | 25 +++++++----- .../test_database/test_crud/test_users.py | 38 ++++++++++++++++++- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index 5cb27652f..646684822 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -43,6 +43,7 @@ def add_coach(db: Session, 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) def remove_coach(db: Session, user_id: int, edition_name: str): diff --git a/backend/src/database/crud/users.py b/backend/src/database/crud/users.py index 206d4fdbe..31dba222b 100644 --- a/backend/src/database/crud/users.py +++ b/backend/src/database/crud/users.py @@ -63,10 +63,10 @@ def get_users_filtered( exclude_edition = get_edition_by_name(db, exclude_edition_name) 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) - ) + User.user_id.not_in( + db.query(user_editions.c.user_id).where(user_editions.c.edition_id == exclude_edition.edition_id) ) + ) return paginate(query, page).all() @@ -130,12 +130,12 @@ def get_requests_page(db: Session, page: int, user_name: str = "") -> list[Coach def _get_requests_for_edition_query(db: Session, edition: Edition, user_name: str = "") -> Query: - return db.query(CoachRequest)\ - .where(CoachRequest.edition_id == edition.edition_id)\ - .join(User)\ - .where(User.name.contains(user_name))\ - .join(AuthEmail, isouter=True)\ - .join(AuthGitHub, isouter=True)\ + return db.query(CoachRequest) \ + .where(CoachRequest.edition_id == edition.edition_id) \ + .join(User) \ + .where(User.name.contains(user_name)) \ + .join(AuthEmail, isouter=True) \ + .join(AuthGitHub, isouter=True) \ .join(AuthGoogle, isouter=True) @@ -175,3 +175,10 @@ def reject_request(db: Session, request_id: int): """ db.query(CoachRequest).where(CoachRequest.request_id == request_id).delete() db.commit() + + +def remove_request_if_exists(db: Session, 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() diff --git a/backend/tests/test_database/test_crud/test_users.py b/backend/tests/test_database/test_crud/test_users.py index 2c0631cdc..deb27a51f 100644 --- a/backend/tests/test_database/test_crud/test_users.py +++ b/backend/tests/test_database/test_crud/test_users.py @@ -634,16 +634,50 @@ 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() # Create request request1 = models.CoachRequest(user_id=user1.user_id, edition_id=edition1.edition_id) database_session.add(request1) - database_session.commit() users_crud.reject_request(database_session, request1.request_id) requests = database_session.query(CoachRequest).all() assert len(requests) == 0 + + +def test_remove_request_if_exists_exists(database_session: Session): + """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() + + request = models.CoachRequest(user_id=user.user_id, edition_id=edition.edition_id) + database_session.add(request) + database_session.commit() + + assert database_session.query(CoachRequest).count() == 1 + + # Remove the request + users_crud.remove_request_if_exists(database_session, user.user_id, edition.name) + + assert database_session.query(CoachRequest).count() == 0 + + +def test_remove_request_if_not_exists(database_session: Session): + """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() + + # 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) From b9129bab308525069d138f71ec2ecb91f474a6fb Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 18 Apr 2022 12:05:23 +0200 Subject: [PATCH 423/536] Use starlette status instead of ints --- backend/src/app/routers/users/users.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index 9d9b2fe17..2e82e5f01 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -1,5 +1,6 @@ from fastapi import APIRouter, Query, Depends from sqlalchemy.orm import Session +from starlette import status import src.app.logic.users as logic from src.app.routers.tags import Tags @@ -38,7 +39,7 @@ async def get_current_user(db: Session = Depends(get_session), user: UserDB = De return user_data -@users_router.patch("/{user_id}", status_code=204, dependencies=[Depends(require_admin)]) +@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)): """ Set admin-status of user @@ -46,7 +47,8 @@ async def patch_admin_status(user_id: int, admin: AdminPatch, db: Session = Depe logic.edit_admin_status(db, user_id, admin) -@users_router.post("/{user_id}/editions/{edition_name}", status_code=204, dependencies=[Depends(require_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)): """ Add user as coach of the given edition @@ -54,7 +56,8 @@ async def add_to_edition(user_id: int, edition_name: str, db: Session = Depends( logic.add_coach(db, user_id, edition_name) -@users_router.delete("/{user_id}/editions/{edition_name}", status_code=204, dependencies=[Depends(require_admin)]) +@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)): """ Remove user as coach of the given edition @@ -62,7 +65,8 @@ async def remove_from_edition(user_id: int, edition_name: str, db: Session = Dep logic.remove_coach(db, user_id, edition_name) -@users_router.delete("/{user_id}/editions", status_code=204, dependencies=[Depends(require_admin)]) +@users_router.delete("/{user_id}/editions", status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(require_admin)]) async def remove_from_all_editions(user_id: int, db: Session = Depends(get_session)): """ Remove user as coach from all editions @@ -82,7 +86,8 @@ async def get_requests( return logic.get_request_list(db, edition, user, page) -@users_router.post("/requests/{request_id}/accept", status_code=204, dependencies=[Depends(require_admin)]) +@users_router.post("/requests/{request_id}/accept", status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(require_admin)]) async def accept_request(request_id: int, db: Session = Depends(get_session)): """ Accept a coach request @@ -90,7 +95,8 @@ async def accept_request(request_id: int, db: Session = Depends(get_session)): logic.accept_request(db, request_id) -@users_router.post("/requests/{request_id}/reject", status_code=204, dependencies=[Depends(require_admin)]) +@users_router.post("/requests/{request_id}/reject", status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(require_admin)]) async def reject_request(request_id: int, db: Session = Depends(get_session)): """ Reject a coach request From b1343037fc62bb5d15cafbdbd90b09d8e954f566 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Mon, 18 Apr 2022 12:34:14 +0200 Subject: [PATCH 424/536] remove old invite when creating mailto link --- backend/src/app/logic/invites.py | 4 +++- backend/src/database/crud/invites.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/src/app/logic/invites.py b/backend/src/app/logic/invites.py index a835984ea..d060b8363 100644 --- a/backend/src/app/logic/invites.py +++ b/backend/src/app/logic/invites.py @@ -19,7 +19,9 @@ def get_pending_invites_page(db: Session, edition: Edition, page: int) -> Invite def create_mailto_link(db: Session, edition: Edition, email_address: EmailAddress) -> NewInviteLink: """Add a new invite link into the database & return a mailto link for it""" - # Create db entry + # Create db entry, drop existing. + if invite := crud.get_optional_invite_link_by_edition_and_email(db, edition, email_address.email) is not None: + crud.delete_invite_link(db, invite) new_link_db = crud.create_invite_link(db, edition, email_address.email) # Create endpoint for the user to click on diff --git a/backend/src/database/crud/invites.py b/backend/src/database/crud/invites.py index 90fe70a4c..c12002faa 100644 --- a/backend/src/database/crud/invites.py +++ b/backend/src/database/crud/invites.py @@ -38,6 +38,10 @@ def get_pending_invites_for_edition_page(db: Session, edition: Edition, page: in return paginate(_get_pending_invites_for_edition_query(db, edition), page).all() +def get_optional_invite_link_by_edition_and_email(db: Session, edition: Edition, email: str) -> InviteLink | None: + return db.query(InviteLink).where(InviteLink.edition == edition and InviteLink.target_email == email).one_or_none() + + def get_invite_link_by_uuid(db: Session, 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 7ea80a532c90334f6f37a1ca5300c3790c88dbc0 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Mon, 18 Apr 2022 12:39:17 +0200 Subject: [PATCH 425/536] fix typing and linting --- backend/src/app/logic/invites.py | 3 ++- backend/src/database/crud/invites.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/app/logic/invites.py b/backend/src/app/logic/invites.py index d060b8363..d62babd17 100644 --- a/backend/src/app/logic/invites.py +++ b/backend/src/app/logic/invites.py @@ -20,7 +20,8 @@ def get_pending_invites_page(db: Session, edition: Edition, page: int) -> Invite def create_mailto_link(db: Session, 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. - if invite := crud.get_optional_invite_link_by_edition_and_email(db, edition, email_address.email) is not None: + invite = crud.get_optional_invite_link_by_edition_and_email(db, edition, email_address.email) + if invite is not None: crud.delete_invite_link(db, invite) new_link_db = crud.create_invite_link(db, edition, email_address.email) diff --git a/backend/src/database/crud/invites.py b/backend/src/database/crud/invites.py index c12002faa..988a52ba4 100644 --- a/backend/src/database/crud/invites.py +++ b/backend/src/database/crud/invites.py @@ -39,6 +39,7 @@ def get_pending_invites_for_edition_page(db: Session, edition: Edition, page: in def get_optional_invite_link_by_edition_and_email(db: Session, edition: Edition, email: str) -> InviteLink | None: + """Return an optional invite link by edition and target_email""" return db.query(InviteLink).where(InviteLink.edition == edition and InviteLink.target_email == email).one_or_none() From 9c638de3b1dd83c999b6a4435884049a2671b9c4 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Mon, 18 Apr 2022 12:44:41 +0200 Subject: [PATCH 426/536] use old invite instead of new one --- backend/src/app/logic/invites.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/src/app/logic/invites.py b/backend/src/app/logic/invites.py index d62babd17..1a29c979b 100644 --- a/backend/src/app/logic/invites.py +++ b/backend/src/app/logic/invites.py @@ -21,12 +21,11 @@ def create_mailto_link(db: Session, edition: Edition, email_address: EmailAddres """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) - if invite is not None: - crud.delete_invite_link(db, invite) - new_link_db = crud.create_invite_link(db, edition, email_address.email) + if invite is None: + invite = crud.create_invite_link(db, edition, email_address.email) # Create endpoint for the user to click on - link = f"{settings.FRONTEND_URL}/register/{new_link_db.uuid}" + link = f"{settings.FRONTEND_URL}/register/{invite.uuid}" return NewInviteLink(mail_to=generate_mailto_string( recipient=email_address.email, subject=f"Open Summer Of Code {edition.year} invitation", From 452c5c0a83dd6bf75d538ed412150acd615df038 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq <60451863+stijndcl@users.noreply.github.com> Date: Mon, 18 Apr 2022 13:03:02 +0200 Subject: [PATCH 427/536] Update backend/src/app/logic/invites.py Co-authored-by: Francis <44001949+FKD13@users.noreply.github.com> --- backend/src/app/logic/invites.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/app/logic/invites.py b/backend/src/app/logic/invites.py index 44a1d55be..2d1c486da 100644 --- a/backend/src/app/logic/invites.py +++ b/backend/src/app/logic/invites.py @@ -34,8 +34,8 @@ def create_mailto_link(db: Session, edition: Edition, email_address: EmailAddres new_link_db = crud.create_invite_link(db, edition, email_address.email) # Add edition name & encode with base64 - encoded_uuid = f"{new_link_db.edition.name}/{new_link_db.uuid}".encode("ascii") - encoded_link = base64.b64encode(encoded_uuid).decode("ascii") + encoded_uuid = f"{new_link_db.edition.name}/{new_link_db.uuid}".encode("utf-8") + encoded_link = base64.b64encode(encoded_uuid).decode("utf-8") # Create endpoint for the user to click on link = f"{settings.FRONTEND_URL}/register/{encoded_link}" From 1e068b48c1ae2a48e9a3a350dee55c18dd0d84c7 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Wed, 13 Apr 2022 17:05:17 +0200 Subject: [PATCH 428/536] Always show basic navbar, add href to login page --- frontend/src/components/Navbar/Brand.tsx | 2 +- frontend/src/components/Navbar/Navbar.tsx | 14 +++++--------- frontend/src/components/Navbar/NavbarBase.tsx | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/Navbar/NavbarBase.tsx diff --git a/frontend/src/components/Navbar/Brand.tsx b/frontend/src/components/Navbar/Brand.tsx index 2a863d493..202f1158f 100644 --- a/frontend/src/components/Navbar/Brand.tsx +++ b/frontend/src/components/Navbar/Brand.tsx @@ -5,7 +5,7 @@ import { BSBrand } from "./styles"; */ export default function Brand() { return ( - + ; } // User is logged in: safe to try and parse the location now @@ -55,9 +54,7 @@ export default function Navbar() { } return ( - - - + {/* Make Navbar responsive (hamburger menu) */} @@ -76,7 +73,6 @@ export default function Navbar() { - - + ); } diff --git a/frontend/src/components/Navbar/NavbarBase.tsx b/frontend/src/components/Navbar/NavbarBase.tsx new file mode 100644 index 000000000..5c0c16680 --- /dev/null +++ b/frontend/src/components/Navbar/NavbarBase.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import Container from "react-bootstrap/Container"; +import { BSNavbar } from "./styles"; +import Brand from "./Brand"; + +/** + * Base component for the Navbar that is displayed at all times to allow + * basic navigation + */ +export default function NavbarBase({ children }: { children?: React.ReactNode }) { + return ( + + + + {children} + + + ); +} From 6f1ee1fe0660cfdf9b9dd614868fd9630fcc3e45 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 18 Apr 2022 15:10:07 +0200 Subject: [PATCH 429/536] Wrap all routes in private route --- frontend/src/Router.tsx | 69 +++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index a275eebae..c26980435 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -45,47 +45,50 @@ export default function Router() { {/* Redirect /login to the login page */} } /> } /> - }> - {/* TODO admins page */} - } /> - - }> - } /> - }> - {/* TODO create edition page */} - } /> - - }> - {/* TODO edition page? do we need? maybe just some nav/links? */} + {/* Catch all routes in a PrivateRoute, so you can't visit them */} + {/* unless you are logged in */} + }> + }> + {/* TODO admins page */} } /> + + }> + } /> + }> + {/* TODO create edition page */} + } /> + + }> + {/* TODO edition page? do we need? maybe just some nav/links? */} + } /> - {/* Projects routes */} - }> - } /> - }> - {/* TODO create project page */} - } /> + {/* Projects routes */} + }> + } /> + }> + {/* TODO create project page */} + } /> + + {/* TODO project page */} + } /> - {/* TODO project page */} - } /> - - {/* Students routes */} - } /> - {/* TODO student page */} - } /> - {/* TODO student emails page */} - } /> + {/* Students routes */} + } /> + {/* TODO student page */} + } /> + {/* TODO student emails page */} + } /> - {/* Users routes */} - }> - } /> + {/* Users routes */} + }> + } /> + - } /> - } /> - } /> + } /> + } /> } /> )} From a2653337f386af10d07011df4e9b8587b46ff6be Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 18 Apr 2022 15:12:12 +0200 Subject: [PATCH 430/536] Forgot some --- frontend/src/Router.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index c26980435..c3d8f31ac 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -86,10 +86,13 @@ export default function Router() { + } /> + } /> + } + /> - } /> - } /> - } /> )} From 646cb8a9935fd8a1b2aca978a6aa339a6ac2fcba Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 18 Apr 2022 15:28:52 +0200 Subject: [PATCH 431/536] Remove pending page & replace with temporary component rendered after registering --- .../src/views/RegisterPage/RegisterPage.tsx | 70 +++++++++++-------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/frontend/src/views/RegisterPage/RegisterPage.tsx b/frontend/src/views/RegisterPage/RegisterPage.tsx index 014279db8..dcc831cdc 100644 --- a/frontend/src/views/RegisterPage/RegisterPage.tsx +++ b/frontend/src/views/RegisterPage/RegisterPage.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; +import { useParams } from "react-router-dom"; import { register } from "../../utils/api/register"; import { validateRegistrationUrl } from "../../utils/api"; @@ -16,6 +16,7 @@ import { import { RegisterFormContainer, Or, RegisterButton } from "./styles"; import { decodeRegistrationLink } from "../../utils/logic/registration"; +import PendingPage from "../PendingPage"; /** * Page where a user can register a new account. If the uuid in the url is invalid, @@ -23,9 +24,16 @@ import { decodeRegistrationLink } from "../../utils/logic/registration"; */ export default function RegisterPage() { const [validUuid, setValidUuid] = useState(false); + const [pending, setPending] = useState(false); const params = useParams(); const data = decodeRegistrationLink(params.uuid); + // Form fields + const [email, setEmail] = useState(""); + const [name, setName] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + useEffect(() => { async function validateUuid() { if (data) { @@ -55,7 +63,7 @@ export default function RegisterPage() { try { const response = await register(edition, email, name, uuid, password); if (response) { - navigate("/pending"); + setPending(true); } } catch (error) { console.log(error); @@ -63,35 +71,35 @@ export default function RegisterPage() { } } - const [email, setEmail] = useState(""); - const [name, setName] = useState(""); - const [password, setPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); + if (pending) { + return ; + } - const navigate = useNavigate(); + // Invalid link + if (!(validUuid && data)) { + return ; + } - if (validUuid && data) { - return ( -
                                    - - - - or - - - - callRegister(data.edition, data.uuid)} - /> -
                                    - callRegister(data.edition, data.uuid)}> - Register - -
                                    -
                                    -
                                    - ); - } else return ; + return ( +
                                    + + + + or + + + + callRegister(data.edition, data.uuid)} + /> +
                                    + callRegister(data.edition, data.uuid)}> + Register + +
                                    +
                                    +
                                    + ); } From f949cef77156adb36f76b3b2b25cda9a5e7cd7a5 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 18 Apr 2022 15:34:57 +0200 Subject: [PATCH 432/536] Run prettier --- frontend/src/components/Navbar/Navbar.tsx | 36 +++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/Navbar/Navbar.tsx b/frontend/src/components/Navbar/Navbar.tsx index 9e8a11c71..48e42f6af 100644 --- a/frontend/src/components/Navbar/Navbar.tsx +++ b/frontend/src/components/Navbar/Navbar.tsx @@ -55,24 +55,24 @@ export default function Navbar() { return ( - {/* Make Navbar responsive (hamburger menu) */} - - - - + {/* Make Navbar responsive (hamburger menu) */} + + + + ); } From 3896b2e93a062c0e277593d25717d3cc21902267 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Mon, 18 Apr 2022 17:19:13 +0200 Subject: [PATCH 433/536] 2 more components for input fields new icon to delete a project --- .../InputFields/Partner/Partner.tsx | 16 +++++++++ .../InputFields/Partner/index.ts | 1 + .../InputFields/Skill/Skill.tsx | 16 +++++++++ .../InputFields/Skill/index.ts | 1 + .../InputFields/index.ts | 2 ++ .../CreateProjectComponents/index.ts | 8 ++++- .../ProjectCard/ProjectCard.tsx | 4 +-- frontend/src/data/interfaces/projects.ts | 2 +- frontend/src/utils/api/projects.ts | 4 +-- .../CreateProjectPage/CreateProjectPage.tsx | 34 +++++++++++-------- 10 files changed, 68 insertions(+), 20 deletions(-) create mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx create mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/index.ts create mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx create mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/index.ts diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx new file mode 100644 index 000000000..385bae14d --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx @@ -0,0 +1,16 @@ +import { Input, AddButton } from "../../styles"; + +export default function Partner({ + partners, + setPartners, +}: { + partners: string[]; + setPartners: (partners: string[]) => void; +}) { + return ( +
                                    + setPartners([])} placeholder="Partner" /> + Add partner +
                                    + ); +} diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/index.ts new file mode 100644 index 000000000..ccf0fba3a --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/index.ts @@ -0,0 +1 @@ +export { default } from "./Partner"; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx new file mode 100644 index 000000000..fc67d4654 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx @@ -0,0 +1,16 @@ +import { Input, AddButton } from "../../styles"; + +export default function Skill({ + skills, + setSkills, +}: { + skills: string[]; + setSkills: (skills: string[]) => void; +}) { + return ( +
                                    + setSkills([])} placeholder="Skill" /> + Add skill +
                                    + ); +} diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/index.ts new file mode 100644 index 000000000..c5a820d40 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/index.ts @@ -0,0 +1 @@ +export { default } from "./Skill"; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts index c34286e21..b0235977d 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts @@ -1,3 +1,5 @@ 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 643b4a8f3..184b4049a 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts @@ -1,2 +1,8 @@ -export { NameInput, NumberOfStudentsInput, CoachInput } from "./InputFields"; +export { + NameInput, + NumberOfStudentsInput, + CoachInput, + SkillInput, + PartnerInput, +} from "./InputFields"; export { default as AddedCoaches } from "./AddedCoaches"; diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 0441a853c..d42e61134 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -14,7 +14,7 @@ import { } from "./styles"; import { BsPersonFill } from "react-icons/bs"; -import { TiDeleteOutline } from "react-icons/ti"; +import { HiOutlineTrash } from "react-icons/hi"; import { useState } from "react"; @@ -60,7 +60,7 @@ export default function ProjectCard({ - + (0); - const [skills, setSkills] = useState([]); - const [partners, setPartners] = useState([]); + const [skills, setSkills] = useState([]); + const [partners, setPartners] = useState([]); // States for coaches const [coach, setCoach] = useState(""); @@ -46,18 +48,22 @@ export default function CreateProjectPage() { /> -
                                    - setSkills([])} placeholder="Skill" /> - Add skill -
                                    -
                                    - setPartners([])} placeholder="Partner" /> - Add partner -
                                    + + - createProject("2022", name, numberOfStudents!, skills, partners, coaches) - } + onClick={async () => { + const response = await createProject( + "2022", + name, + numberOfStudents!, + skills, + partners, + coaches + ); + if (response) { + navigate("/editions/2022/projects/"); + } else alert("Something went wrong :("); + }} > Create Project From e6d5b8e6b100dc19ebd9c209d29bac7af7d214fc Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Tue, 19 Apr 2022 09:56:32 +0200 Subject: [PATCH 434/536] merged the 2 head --- .../versions/d4eaf2b564a4_merge_heads.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 backend/migrations/versions/d4eaf2b564a4_merge_heads.py diff --git a/backend/migrations/versions/d4eaf2b564a4_merge_heads.py b/backend/migrations/versions/d4eaf2b564a4_merge_heads.py new file mode 100644 index 000000000..410472ae3 --- /dev/null +++ b/backend/migrations/versions/d4eaf2b564a4_merge_heads.py @@ -0,0 +1,24 @@ +"""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 From 3175b0b6fefd862a1e88836a79d57a1456b86fc1 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Tue, 19 Apr 2022 10:06:42 +0200 Subject: [PATCH 435/536] made it compatible with the new develop (again) --- backend/src/app/logic/students.py | 2 +- backend/src/app/schemas/students.py | 4 ++-- backend/tests/test_database/test_crud/test_students.py | 4 ++-- .../test_editions/test_students/test_students.py | 5 +++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index 326fa278d..65a9017d1 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -5,7 +5,7 @@ from src.database.crud.skills import get_skills_by_ids from src.database.crud.students import set_definitive_decision_on_student, delete_student, get_students, get_emails from src.database.crud.suggestions import get_suggestions_of_student_by_type -from src.database.enums import DecisionEnum +from src.database.enums import DecisionEnum, EmailStatusEnum from src.database.models import Edition, Student, Skill, DecisionEmail from src.app.schemas.students import ( ReturnStudentList, ReturnStudent, CommonQueryParams, ReturnStudentMailList, diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index 4752b2f77..ffea637b3 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -3,7 +3,7 @@ from pydantic import Field from src.app.schemas.webhooks import CamelCaseModel -from src.database.enums import DecisionEnum +from src.database.enums import DecisionEnum, EmailStatusEnum from src.app.schemas.skills import Skill @@ -79,7 +79,7 @@ class DecisionEmail(CamelCaseModel): """ email_id: int student_id: int - decision: DecisionEnum + decision: EmailStatusEnum date: datetime class Config: diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index ccf4140e1..0bb76738c 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -3,7 +3,7 @@ from sqlalchemy.orm import Session from sqlalchemy.orm.exc import NoResultFound from src.database.models import Student, User, Edition, Skill, DecisionEmail -from src.database.enums import DecisionEnum +from src.database.enums import DecisionEnum, EmailStatusEnum from src.database.crud.students import (get_student_by_id, set_definitive_decision_on_student, delete_student, get_students, get_emails) from src.app.schemas.students import CommonQueryParams @@ -55,7 +55,7 @@ def database_with_data(database_session: Session): # DecisionEmail decision_email: DecisionEmail = DecisionEmail( - student=student01, decision=DecisionEnum.YES, date=datetime.datetime.now()) + student=student01, decision=EmailStatusEnum.APPROVED, date=datetime.datetime.now()) database_session.add(decision_email) database_session.commit() 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 0e9acfbb2..d43c34df7 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 @@ -2,7 +2,7 @@ import pytest from sqlalchemy.orm import Session from starlette import status -from src.database.enums import DecisionEnum +from src.database.enums import DecisionEnum, EmailStatusEnum from src.database.models import Student, Edition, Skill, DecisionEmail from tests.utils.authorization import AuthClient @@ -46,7 +46,7 @@ def database_with_data(database_session: Session) -> Session: # DecisionEmail decision_email: DecisionEmail = DecisionEmail( - student=student01, decision=DecisionEnum.YES, date=datetime.datetime.now()) + student=student01, decision=EmailStatusEnum.APPROVED, date=datetime.datetime.now()) database_session.add(decision_email) database_session.commit() return database_session @@ -293,6 +293,7 @@ def test_get_emails_student_admin(database_with_data: Session, auth_client: Auth """tests that an admin can get the mails of a student""" auth_client.admin() response = auth_client.get("/editions/ed2022/students/1/emails") + print(response.json()) assert response.status_code == status.HTTP_200_OK assert len(response.json()["emails"]) == 1 response = auth_client.get("/editions/ed2022/students/2/emails") From 6469324a82a02cd5c8f56ea3acbbbc1ba2adf44c Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Tue, 19 Apr 2022 16:37:28 +0200 Subject: [PATCH 436/536] pylint errors fixed --- backend/src/app/logic/students.py | 2 +- .../routers/editions/students/suggestions/suggestions.py | 1 - backend/src/database/crud/students.py | 3 ++- backend/src/database/crud/suggestions.py | 7 +++++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index 65a9017d1..326fa278d 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -5,7 +5,7 @@ from src.database.crud.skills import get_skills_by_ids from src.database.crud.students import set_definitive_decision_on_student, delete_student, get_students, get_emails from src.database.crud.suggestions import get_suggestions_of_student_by_type -from src.database.enums import DecisionEnum, EmailStatusEnum +from src.database.enums import DecisionEnum from src.database.models import Edition, Student, Skill, DecisionEmail from src.app.schemas.students import ( ReturnStudentList, ReturnStudent, CommonQueryParams, ReturnStudentMailList, diff --git a/backend/src/app/routers/editions/students/suggestions/suggestions.py b/backend/src/app/routers/editions/students/suggestions/suggestions.py index 927237bf5..113b5aefb 100644 --- a/backend/src/app/routers/editions/students/suggestions/suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/suggestions.py @@ -1,4 +1,3 @@ -# pylint: skip-file from fastapi import APIRouter, Depends from sqlalchemy.orm import Session diff --git a/backend/src/database/crud/students.py b/backend/src/database/crud/students.py index 723e974d6..1d4141463 100644 --- a/backend/src/database/crud/students.py +++ b/backend/src/database/crud/students.py @@ -23,7 +23,8 @@ def delete_student(db: Session, student: Student) -> None: db.commit() -def get_students(db: Session, edition: Edition, commons: CommonQueryParams, skills: list[Skill] = None) -> list[Student]: +def get_students(db: Session, edition: Edition, + commons: CommonQueryParams, skills: list[Skill] = None) -> list[Student]: """Get students""" query = db.query(Student)\ .where(Student.edition == edition)\ diff --git a/backend/src/database/crud/suggestions.py b/backend/src/database/crud/suggestions.py index d806983c4..1f9a9f90a 100644 --- a/backend/src/database/crud/suggestions.py +++ b/backend/src/database/crud/suggestions.py @@ -39,6 +39,9 @@ def update_suggestion(db: Session, suggestion: Suggestion, decision: DecisionEnu db.commit() -def get_suggestions_of_student_by_type(db: Session, student_id: int | None, type: DecisionEnum) -> list[Suggestion]: +def get_suggestions_of_student_by_type(db: Session, 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).all() + return db.query(Suggestion)\ + .where(Suggestion.student_id == student_id)\ + .where(Suggestion.suggestion == type_suggestion).all() From ec4994445504bb901b9f23dacf5e3ced63e3d034 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Tue, 19 Apr 2022 16:43:46 +0200 Subject: [PATCH 437/536] added disable too many arguments --- backend/src/app/schemas/students.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index ffea637b3..198e0050e 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -1,3 +1,4 @@ +# pylint: disable=too-many-arguments from datetime import datetime from fastapi import Query from pydantic import Field From 68c20f57ba248f1481219711d43d9a1712f62e25 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Tue, 19 Apr 2022 17:05:24 +0200 Subject: [PATCH 438/536] fixed type errors --- backend/src/app/logic/students.py | 13 ++++++++++++- backend/src/app/schemas/students.py | 22 +++++++++------------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index 326fa278d..369d17cf4 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -34,7 +34,18 @@ def get_students_search(db: Session, edition: Edition, commons: CommonQueryParam students: list[StudentModel] = [] for student in students_orm: - students.append(student) + 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( diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index 198e0050e..ad683ce49 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -1,7 +1,7 @@ -# pylint: disable=too-many-arguments +from dataclasses import dataclass from datetime import datetime from fastapi import Query -from pydantic import Field +from pydantic import BaseModel, Field from src.app.schemas.webhooks import CamelCaseModel from src.database.enums import DecisionEnum, EmailStatusEnum @@ -40,7 +40,7 @@ class Student(CamelCaseModel): edition_id: int skills: list[Skill] - nr_of_suggestions: Suggestions = None + nr_of_suggestions: Suggestions | None = None class Config: """Set to ORM mode""" @@ -60,18 +60,14 @@ class ReturnStudentList(CamelCaseModel): """ students: list[Student] - +@dataclass class CommonQueryParams: """search query paramaters""" - - def __init__(self, first_name: str = "", last_name: str = "", alumni: bool = False, - student_coach: bool = False, skill_ids: list[int] = Query([])) -> None: - """init""" - self.first_name = first_name - self.last_name = last_name - self.alumni = alumni - self.student_coach = student_coach - self.skill_ids = skill_ids + first_name: str = "" + last_name: str = "" + alumni: bool = False + student_coach: bool = False + skill_ids: list[int] = Query([]) class DecisionEmail(CamelCaseModel): From 51de1a9e07f4435be9d63f0dd283c77d34ca2f69 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Tue, 19 Apr 2022 17:08:59 +0200 Subject: [PATCH 439/536] pylint error fixed --- backend/src/app/schemas/students.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index ad683ce49..5945e782f 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from datetime import datetime from fastapi import Query -from pydantic import BaseModel, Field +from pydantic import Field from src.app.schemas.webhooks import CamelCaseModel from src.database.enums import DecisionEnum, EmailStatusEnum From 4631587b6678c69ceaab6bd84ce176b3425532f4 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Wed, 20 Apr 2022 11:24:55 +0200 Subject: [PATCH 440/536] added pagination and tests --- backend/src/app/schemas/students.py | 1 + backend/src/database/crud/students.py | 3 +- .../test_students/test_students.py | 107 +++++++++++++++++- 3 files changed, 107 insertions(+), 4 deletions(-) diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index 5945e782f..da5cee2ca 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -68,6 +68,7 @@ class CommonQueryParams: alumni: bool = False student_coach: bool = False skill_ids: list[int] = Query([]) + page: int = 0 class DecisionEmail(CamelCaseModel): diff --git a/backend/src/database/crud/students.py b/backend/src/database/crud/students.py index 1d4141463..3db3be515 100644 --- a/backend/src/database/crud/students.py +++ b/backend/src/database/crud/students.py @@ -1,4 +1,5 @@ from sqlalchemy.orm import Session +from src.database.crud.util import paginate from src.database.enums import DecisionEnum from src.database.models import Edition, Skill, Student, DecisionEmail from src.app.schemas.students import CommonQueryParams @@ -43,7 +44,7 @@ def get_students(db: Session, edition: Edition, for skill in skills: query = query.where(Student.skills.contains(skill)) - return query.all() + return paginate(query, commons.page).all() def get_emails(db: Session, student: Student) -> list[DecisionEmail]: 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 d43c34df7..eccd1c5e2 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 @@ -2,6 +2,7 @@ import pytest from sqlalchemy.orm import Session 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 @@ -186,6 +187,25 @@ def test_get_all_students(database_with_data: Session, auth_client: AuthClient): assert len(response.json()["students"]) == 2 +def test_get_all_students_pagination(database_with_data: Session, auth_client: AuthClient): + """tests get all students with pagination""" + edition: Edition = database_with_data.query(Edition).all()[0] + 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): """tests get students based on query paramer first name""" edition: Edition = database_with_data.query(Edition).all()[0] @@ -195,6 +215,27 @@ def test_get_first_name_students(database_with_data: Session, auth_client: AuthC assert len(response.json()["students"]) == 1 +def test_get_first_name_student_pagination(database_with_data: Session, 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) + 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/?first_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/?first_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): """tests get students based on query paramer last name""" edition: Edition = database_with_data.query(Edition).all()[0] @@ -205,6 +246,27 @@ def test_get_last_name_students(database_with_data: Session, auth_client: AuthCl assert len(response.json()["students"]) == 1 +def test_get_last_name_students_pagination(database_with_data: Session, 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) + 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/?last_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/?last_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_alumni_students(database_with_data: Session, auth_client: AuthClient): """tests get students based on query paramer alumni""" edition: Edition = database_with_data.query(Edition).all()[0] @@ -214,6 +276,27 @@ def test_get_alumni_students(database_with_data: Session, auth_client: AuthClien assert len(response.json()["students"]) == 1 +def test_get_alumni_students_pagination(database_with_data: Session, 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) + 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): """tests get students based on query paramer student coach""" edition: Edition = database_with_data.query(Edition).all()[0] @@ -223,6 +306,27 @@ def test_get_student_coach_students(database_with_data: Session, auth_client: Au assert len(response.json()["students"]) == 1 +def test_get_student_coach_students_pagination(database_with_data: Session, 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) + 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): """tests get students based on query paramer one skill""" edition: Edition = database_with_data.query(Edition).all()[0] @@ -239,7 +343,6 @@ def test_get_multiple_skill_students(database_with_data: Session, auth_client: A auth_client.coach(edition) response = auth_client.get( "/editions/ed2022/students/?skill_ids=4&skill_ids=5") - print(response.json()) assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 1 assert response.json()["students"][0]["firstName"] == "Marta" @@ -270,7 +373,6 @@ def test_get_one_real_one_ghost_skill_students(database_with_data: Session, auth auth_client.coach(edition) response = auth_client.get( "/editions/ed2022/students/?skill_ids=4&skill_ids=100") - print(response.json()) assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 0 @@ -293,7 +395,6 @@ def test_get_emails_student_admin(database_with_data: Session, auth_client: Auth """tests that an admin can get the mails of a student""" auth_client.admin() response = auth_client.get("/editions/ed2022/students/1/emails") - print(response.json()) assert response.status_code == status.HTTP_200_OK assert len(response.json()["emails"]) == 1 response = auth_client.get("/editions/ed2022/students/2/emails") From ea1133394dc72282126962881130fbf0dd01c2e8 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 20 Apr 2022 12:57:16 +0200 Subject: [PATCH 441/536] fixed spamming api for projects --- frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index 6758fad04..305c53777 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -55,7 +55,7 @@ export default function ProjectPage() { if (response) { setProjectsAPI(response.projects); setProjects(response.projects); - } else setGotProjects(false); + } } if (!gotProjects) { callProjects(); From 1fbed427d16d6af788cea542e84da00fa2509ceb Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Thu, 21 Apr 2022 02:23:54 +0200 Subject: [PATCH 442/536] just need to write test for filtering on emails --- backend/src/app/exceptions/handlers.py | 14 +- backend/src/app/exceptions/students_email.py | 4 + backend/src/app/logic/students.py | 24 +- .../app/routers/editions/students/students.py | 27 +- backend/src/app/schemas/students.py | 18 +- backend/src/database/crud/students.py | 34 ++- .../test_database/test_crud/test_students.py | 239 +++++++++++++++++- .../test_students/test_students.py | 127 +++++++++- 8 files changed, 470 insertions(+), 17 deletions(-) create mode 100644 backend/src/app/exceptions/students_email.py diff --git a/backend/src/app/exceptions/handlers.py b/backend/src/app/exceptions/handlers.py index 1571d5383..a289dad97 100644 --- a/backend/src/app/exceptions/handlers.py +++ b/backend/src/app/exceptions/handlers.py @@ -9,6 +9,7 @@ from .parsing import MalformedUUIDError from .projects import StudentInConflictException, FailedToAddProjectRoleException from .register import FailedToAddNewUserException +from .students_email import FailedToAddNewEmailException from .webhooks import WebhookProcessException @@ -85,14 +86,16 @@ def failed_to_add_new_user_exception(_request: Request, _exception: FailedToAddN def student_in_conflict_exception(_request: Request, _exception: StudentInConflictException): return JSONResponse( status_code=status.HTTP_409_CONFLICT, - content={'message': 'Resolve the conflict this student is in before confirming their role'} + content={ + 'message': 'Resolve the conflict this student is in before confirming their role'} ) @app.exception_handler(FailedToAddProjectRoleException) def failed_to_add_project_role_exception(_request: Request, _exception: FailedToAddProjectRoleException): return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, - content={'message': 'Something went wrong while adding this student to the project'} + content={ + 'message': 'Something went wrong while adding this student to the project'} ) @app.exception_handler(ReadOnlyEditionException) @@ -101,3 +104,10 @@ def read_only_edition_exception(_request: Request, _exception: ReadOnlyEditionEx status_code=status.HTTP_405_METHOD_NOT_ALLOWED, content={'message': 'This edition is Read-Only'} ) + + @app.exception_handler(FailedToAddNewEmailException) + def failed_to_add_new_email_exception(_request: Request, _exception: FailedToAddNewEmailException): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={'message': 'Something went wrong while creating a new email'} + ) diff --git a/backend/src/app/exceptions/students_email.py b/backend/src/app/exceptions/students_email.py new file mode 100644 index 000000000..ba34f1951 --- /dev/null +++ b/backend/src/app/exceptions/students_email.py @@ -0,0 +1,4 @@ +class FailedToAddNewEmailException(Exception): + """ + Exception raised when a new email can't be added + """ diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index 369d17cf4..96be35ba0 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -1,15 +1,20 @@ from sqlalchemy.orm import Session from sqlalchemy.orm.exc import NoResultFound +from src.app.exceptions.students_email import FailedToAddNewEmailException from src.app.schemas.students import NewDecision from src.database.crud.skills import get_skills_by_ids -from src.database.crud.students import set_definitive_decision_on_student, delete_student, get_students, get_emails +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.app.schemas.students import ( ReturnStudentList, ReturnStudent, CommonQueryParams, ReturnStudentMailList, - Student as StudentModel, Suggestions as SuggestionsModel) + Student as StudentModel, Suggestions as SuggestionsModel, + NewEmail, DecisionEmail as DecionEmailModel, EmailsSearchQueryParams) def definitive_decision_on_student(db: Session, student: Student, decision: NewDecision) -> None: @@ -71,3 +76,18 @@ def get_emails_of_student(db: Session, edition: Edition, student: Student) -> Re raise NoResultFound emails: list[DecisionEmail] = get_emails(db, student) return ReturnStudentMailList(emails=emails) + + +def make_new_email(db: Session, edition: Edition, new_email: NewEmail) -> DecionEmailModel: + """make a new email""" + student = get_student_by_id(db, new_email.student_id) + if student.edition != edition: + raise FailedToAddNewEmailException + email: DecisionEmail = create_email(db, student, new_email.email_status) + return email + + +def last_emails_of_students(db: Session, edition: Edition, commons: EmailsSearchQueryParams) -> ReturnStudentMailList: + """get last emails of students with search params""" + emails: list[DecisionEmail] = get_last_emails_of_students(db, edition, commons) + return ReturnStudentMailList(emails=emails) diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index b54202268..034c31ea9 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -1,4 +1,3 @@ -# pylint: skip-file from fastapi import APIRouter, Depends from sqlalchemy.orm import Session @@ -6,8 +5,11 @@ from src.app.routers.tags import Tags from src.app.utils.dependencies import get_student, get_edition, require_admin, require_auth from src.app.logic.students import ( - definitive_decision_on_student, remove_student, get_student_return, get_students_search, get_emails_of_student) -from src.app.schemas.students import NewDecision, CommonQueryParams, ReturnStudent, ReturnStudentList, ReturnStudentMailList + 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, DecisionEmail, NewEmail, EmailsSearchQueryParams) from src.database.database import get_session from src.database.models import Student, Edition from .suggestions import students_suggestions_router @@ -26,11 +28,24 @@ async def get_students(db: Session = Depends(get_session), """ return get_students_search(db, edition, commons) -@students_router.post("/emails") -async def send_emails(edition: Edition = Depends(get_edition)): + +@students_router.post("/emails", dependencies=[Depends(require_admin)], + status_code=status.HTTP_201_CREATED, response_model=DecisionEmail) +async def send_emails(new_email: NewEmail, db: Session = Depends(get_session), edition: Edition = Depends(get_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=ReturnStudentMailList) +async def get_emails(db: Session = Depends(get_session), edition: Edition = Depends(get_edition), + commons: EmailsSearchQueryParams = Depends(EmailsSearchQueryParams)): """ - Send a Yes/Maybe/No email to a list of students. + Get last emails of students """ + return last_emails_of_students(db, edition, commons) @students_router.delete("/{student_id}", dependencies=[Depends(require_admin)], status_code=status.HTTP_204_NO_CONTENT) diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index da5cee2ca..0842bb92e 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -60,6 +60,7 @@ class ReturnStudentList(CamelCaseModel): """ students: list[Student] + @dataclass class CommonQueryParams: """search query paramaters""" @@ -71,12 +72,21 @@ class CommonQueryParams: page: int = 0 +@dataclass +class EmailsSearchQueryParams: + """search query paramaters for email""" + first_name: str = "" + last_name: str = "" + email_status: EmailStatusEnum | None = None + page: int = 0 + + class DecisionEmail(CamelCaseModel): """ Model to represent DecisionEmail """ email_id: int - student_id: int + student: Student decision: EmailStatusEnum date: datetime @@ -90,3 +100,9 @@ class ReturnStudentMailList(CamelCaseModel): Model to return a list of mails of a student """ emails: list[DecisionEmail] + + +class NewEmail(CamelCaseModel): + """The fields of a DecisionEmail""" + student_id: int + email_status: EmailStatusEnum diff --git a/backend/src/database/crud/students.py b/backend/src/database/crud/students.py index 3db3be515..a77a4e9c0 100644 --- a/backend/src/database/crud/students.py +++ b/backend/src/database/crud/students.py @@ -1,8 +1,10 @@ +from datetime import datetime from sqlalchemy.orm import Session +from sqlalchemy.sql.expression import func from src.database.crud.util import paginate -from src.database.enums import DecisionEnum +from src.database.enums import DecisionEnum, EmailStatusEnum from src.database.models import Edition, Skill, Student, DecisionEmail -from src.app.schemas.students import CommonQueryParams +from src.app.schemas.students import CommonQueryParams, EmailsSearchQueryParams def get_student_by_id(db: Session, student_id: int) -> Student: @@ -50,3 +52,31 @@ def get_students(db: Session, edition: Edition, def get_emails(db: Session, student: Student) -> list[DecisionEmail]: """Get all emails send to a student""" return db.query(DecisionEmail).where(DecisionEmail.student_id == student.student_id).all() + + +def create_email(db: Session, 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() + return email + + +def get_last_emails_of_students(db: Session, 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.contains(commons.first_name))\ + .where(Student.last_name.contains(commons.last_name))\ + .group_by(DecisionEmail.student_id).subquery() + + emails = db.query(DecisionEmail).join( + last_emails, DecisionEmail.email_id == last_emails.c.email_id + ) + if commons.email_status: + emails = emails.where(DecisionEmail.decision == commons.email_status) + + emails = emails.order_by(DecisionEmail.student_id) + return paginate(emails, commons.page).all() diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index 0bb76738c..91cf5a447 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -4,9 +4,10 @@ from sqlalchemy.orm.exc import NoResultFound from src.database.models import Student, User, Edition, Skill, DecisionEmail from src.database.enums import DecisionEnum, EmailStatusEnum -from src.database.crud.students import (get_student_by_id, set_definitive_decision_on_student, +from src.database.crud.students import (create_email, get_last_emails_of_students, get_student_by_id, + set_definitive_decision_on_student, delete_student, get_students, get_emails) -from src.app.schemas.students import CommonQueryParams +from src.app.schemas.students import CommonQueryParams, EmailsSearchQueryParams @pytest.fixture @@ -183,3 +184,237 @@ def test_get_emails(database_with_data: Session): student = get_student_by_id(database_with_data, 2) emails: list[DecisionEmail] = get_emails(database_with_data, student) assert len(emails) == 0 + + +def test_create_email_applied(database_with_data: Session): + """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) + assert len(emails) == 1 + assert emails[0].decision == EmailStatusEnum.APPLIED + + +def test_create_email_awaiting_project(database_with_data: Session): + """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) + assert len(emails) == 1 + assert emails[0].decision == EmailStatusEnum.AWAITING_PROJECT + + +def test_create_email_approved(database_with_data: Session): + """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) + assert len(emails) == 1 + assert emails[0].decision == EmailStatusEnum.APPROVED + + +def test_create_email_contract_confirmed(database_with_data: Session): + """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) + assert len(emails) == 1 + assert emails[0].decision == EmailStatusEnum.CONTRACT_CONFIRMED + + +def test_create_email_contract_declined(database_with_data: Session): + """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) + assert len(emails) == 1 + assert emails[0].decision == EmailStatusEnum.CONTRACT_DECLINED + + +def test_create_email_rejected(database_with_data: Session): + """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) + assert len(emails) == 1 + assert emails[0].decision == EmailStatusEnum.REJECTED + + +def test_get_last_emails_of_students(database_with_data: Session): + """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) + 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) + + emails: list[DecisionEmail] = get_last_emails_of_students( + database_with_data, edition, EmailsSearchQueryParams()) + assert len(emails) == 2 + assert emails[0].student_id == 1 + assert emails[0].decision == EmailStatusEnum.CONTRACT_CONFIRMED + assert emails[1].student_id == 2 + assert emails[1].decision == EmailStatusEnum.REJECTED + + +def test_get_last_emails_of_students_filter_applied(database_with_data: Session): + """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( + database_with_data, edition, EmailsSearchQueryParams(email_status=EmailStatusEnum.APPLIED)) + + assert len(emails) == 1 + assert emails[0].student_id == 2 + assert emails[0].decision == EmailStatusEnum.APPLIED + + +def test_get_last_emails_of_students_filter_awaiting_project(database_with_data: Session): + """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( + database_with_data, edition, EmailsSearchQueryParams(email_status=EmailStatusEnum.AWAITING_PROJECT)) + + assert len(emails) == 1 + assert emails[0].student_id == 2 + assert emails[0].decision == EmailStatusEnum.AWAITING_PROJECT + + +def test_get_last_emails_of_students_filter_approved(database_with_data: Session): + """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( + database_with_data, edition, EmailsSearchQueryParams(email_status=EmailStatusEnum.APPROVED)) + + assert len(emails) == 1 + assert emails[0].student_id == 2 + assert emails[0].decision == EmailStatusEnum.APPROVED + + +def test_get_last_emails_of_students_filter_contract_confirmed(database_with_data: Session): + """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( + database_with_data, edition, EmailsSearchQueryParams(email_status=EmailStatusEnum.CONTRACT_CONFIRMED)) + + assert len(emails) == 1 + assert emails[0].student_id == 2 + assert emails[0].decision == EmailStatusEnum.CONTRACT_CONFIRMED + + +def test_get_last_emails_of_students_filter_contract_declined(database_with_data: Session): + """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( + database_with_data, edition, EmailsSearchQueryParams(email_status=EmailStatusEnum.CONTRACT_DECLINED)) + + assert len(emails) == 1 + assert emails[0].student_id == 2 + assert emails[0].decision == EmailStatusEnum.CONTRACT_DECLINED + + +def test_get_last_emails_of_students_filter_rejected(database_with_data: Session): + """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( + database_with_data, edition, EmailsSearchQueryParams(email_status=EmailStatusEnum.REJECTED)) + + assert len(emails) == 1 + assert emails[0].student_id == 2 + assert emails[0].decision == EmailStatusEnum.REJECTED + + +def test_get_last_emails_of_students_first_name(database_with_data: Session): + """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( + database_with_data, edition, EmailsSearchQueryParams(first_name="Jos")) + + assert len(emails) == 1 + assert emails[0].student_id == 1 + assert emails[0].decision == EmailStatusEnum.CONTRACT_CONFIRMED + + +def test_get_last_emails_of_students_last_name(database_with_data: Session): + """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( + database_with_data, edition, EmailsSearchQueryParams(last_name="Vermeulen")) + + assert len(emails) == 1 + assert emails[0].student_id == 1 + assert emails[0].decision == EmailStatusEnum.CONTRACT_CONFIRMED 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 eccd1c5e2..bb3c62656 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 @@ -294,7 +294,7 @@ def test_get_alumni_students_pagination(database_with_data: Session, auth_client "/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 + 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): @@ -324,7 +324,7 @@ def test_get_student_coach_students_pagination(database_with_data: Session, auth "/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 + 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): @@ -400,3 +400,126 @@ def test_get_emails_student_admin(database_with_data: Session, auth_client: Auth response = auth_client.get("/editions/ed2022/students/2/emails") assert response.status_code == status.HTTP_200_OK assert len(response.json()["emails"]) == 0 + + +def test_post_email_no_authorization(database_with_data: Session, auth_client: AuthClient): + """tests user need to be loged in""" + response = 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): + """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") + assert response.status_code == status.HTTP_403_FORBIDDEN + + +def test_post_email_applied(database_with_data: Session, auth_client: AuthClient): + """test create email applied""" + auth_client.admin() + response = auth_client.post("/editions/ed2022/students/emails", + json={"student_id": 2, "email_status": 0}) + assert response.status_code == status.HTTP_201_CREATED + assert EmailStatusEnum( + response.json()["decision"]) == EmailStatusEnum.APPLIED + + +def test_post_email_awaiting_project(database_with_data: Session, auth_client: AuthClient): + """test create email awaiting project""" + auth_client.admin() + response = auth_client.post("/editions/ed2022/students/emails", + json={"student_id": 2, "email_status": 1}) + assert response.status_code == status.HTTP_201_CREATED + assert EmailStatusEnum( + response.json()["decision"]) == EmailStatusEnum.AWAITING_PROJECT + + +def test_post_email_approved(database_with_data: Session, auth_client: AuthClient): + """test create email applied""" + auth_client.admin() + response = auth_client.post("/editions/ed2022/students/emails", + json={"student_id": 2, "email_status": 2}) + assert response.status_code == status.HTTP_201_CREATED + assert EmailStatusEnum( + response.json()["decision"]) == EmailStatusEnum.APPROVED + + +def test_post_email_contract_confirmed(database_with_data: Session, auth_client: AuthClient): + """test create email contract confirmed""" + auth_client.admin() + response = auth_client.post("/editions/ed2022/students/emails", + json={"student_id": 2, "email_status": 3}) + assert response.status_code == status.HTTP_201_CREATED + assert EmailStatusEnum( + response.json()["decision"]) == EmailStatusEnum.CONTRACT_CONFIRMED + + +def test_post_email_contract_declined(database_with_data: Session, auth_client: AuthClient): + """test create email contract declined""" + auth_client.admin() + response = auth_client.post("/editions/ed2022/students/emails", + json={"student_id": 2, "email_status": 4}) + assert response.status_code == status.HTTP_201_CREATED + assert EmailStatusEnum( + response.json()["decision"]) == EmailStatusEnum.CONTRACT_DECLINED + + +def test_post_email_rejected(database_with_data: Session, auth_client: AuthClient): + """test create email rejected""" + auth_client.admin() + response = auth_client.post("/editions/ed2022/students/emails", + json={"student_id": 2, "email_status": 5}) + assert response.status_code == status.HTTP_201_CREATED + assert EmailStatusEnum( + response.json()["decision"]) == EmailStatusEnum.REJECTED + + +def test_creat_email_for_ghost(database_with_data: Session, auth_client: AuthClient): + """test create email for student that don't exist""" + auth_client.admin() + response = auth_client.post("/editions/ed2022/students/emails", + json={"student_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""" + 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={"student_id": 3, "email_status": 5}) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +def test_get_emails_no_authorization(database_with_data: Session, auth_client: AuthClient): + """test get emails not loged in""" + response = 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): + """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") + assert response.status_code == status.HTTP_403_FORBIDDEN + + +def test_get_emails(database_with_data: Session, auth_client: AuthClient): + """test get emails""" + auth_client.admin() + auth_client.post("/editions/ed2022/students/emails", + json={"student_id": 1, "email_status": 3}) + auth_client.post("/editions/ed2022/students/emails", + json={"student_id": 2, "email_status": 5}) + response = auth_client.get("/editions/ed2022/students/emails") + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["emails"]) == 2 From 69cc380598c46aaa33e0eaebe2cca652cebc1815 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Thu, 21 Apr 2022 15:42:32 +0200 Subject: [PATCH 443/536] fix after merge --- backend/src/app/logic/users.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/app/logic/users.py b/backend/src/app/logic/users.py index 1fdc69c3a..ed64f2f9c 100644 --- a/backend/src/app/logic/users.py +++ b/backend/src/app/logic/users.py @@ -2,7 +2,7 @@ import src.database.crud.users as users_crud from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, user_model_to_schema, \ - FilterParameters + FilterParameters, UserRequest from src.database.models import User @@ -67,7 +67,12 @@ def get_request_list(db: Session, edition_name: str | None, user_name: str | Non else: requests = users_crud.get_requests_for_edition_page(db, edition_name, page, user_name) - return UserRequestsResponse(requests=requests) + requests_model = [] + for request in requests: + user_req = UserRequest(request_id=request.request_id, edition=request.edition, + user=user_model_to_schema(request.user)) + requests_model.append(user_req) + return UserRequestsResponse(requests=requests_model) def accept_request(db: Session, request_id: int): From a1b65613e77bb6ff358658245e4cb38e4832d73b Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Thu, 21 Apr 2022 15:46:37 +0200 Subject: [PATCH 444/536] Remove admin button on coaches page --- .../src/components/GeneralComponents/AuthTypeIcon.tsx | 1 - frontend/src/views/UsersPage/UsersPage.tsx | 6 ++---- frontend/src/views/UsersPage/styles.ts | 8 -------- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx b/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx index c79f8285c..dcbe28834 100644 --- a/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx +++ b/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx @@ -6,7 +6,6 @@ import { AiFillGithub, AiFillGoogleCircle, AiOutlineQuestionCircle } from "react * @param props.type email/github/google */ export default function AuthTypeIcon(props: { type: string }) { - console.log(props.type); if (props.type === "email") { return ; } else if (props.type === "github") { diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index d1ddfe44f..5e709bb0f 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; -import { useParams, useNavigate } from "react-router-dom"; -import { UsersPageDiv, AdminsButton, UsersHeader } from "./styles"; +import { useParams } from "react-router-dom"; +import { UsersPageDiv, UsersHeader } from "./styles"; import { Coaches } from "../../components/UsersComponents/Coaches"; import { InviteUser } from "../../components/UsersComponents/InviteUser"; import { PendingRequests } from "../../components/UsersComponents/Requests"; @@ -20,7 +20,6 @@ function UsersPage() { const [searchTerm, setSearchTerm] = useState(""); // The word set in filter for coachlist const params = useParams(); - const navigate = useNavigate(); /** * Request a page from the list of coaches. @@ -106,7 +105,6 @@ function UsersPage() {

                                    Manage coaches from {params.editionId}

                                    - navigate("/admins")}>Edit Admins
                                    diff --git a/frontend/src/views/UsersPage/styles.ts b/frontend/src/views/UsersPage/styles.ts index d7adfe0ae..02aaf4d40 100644 --- a/frontend/src/views/UsersPage/styles.ts +++ b/frontend/src/views/UsersPage/styles.ts @@ -1,15 +1,7 @@ import styled from "styled-components"; -import { Button } from "react-bootstrap"; export const UsersPageDiv = styled.div``; -export const AdminsButton = styled(Button)` - background-color: var(--osoc_green); - margin-right: 10px; - margin-top: 10px; - float: right; -`; - export const UsersHeader = styled.div` padding-left: 10px; margin-top: 10px; From 518377bb9df38aa06432096af36f6d8eff9bfbd2 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Thu, 21 Apr 2022 16:25:43 +0200 Subject: [PATCH 445/536] Manual: admins --- files/user_manual.md | 51 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/files/user_manual.md b/files/user_manual.md index 2756901d7..5ebe2221a 100644 --- a/files/user_manual.md +++ b/files/user_manual.md @@ -17,3 +17,54 @@ There are different ways to log in, depending on the way in which you have regis ## Google 1. Click the "Log in" button with the Google logo. + + + +## 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. + +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. + +### Coaches + +The coaches pages is used to manage all coaches of the current selected edition. The page consists of three main parts. + +#### Invite a user + +At the top left, you can invite someone via an invite link. You can choose between creating an email to the person or copying the link. The new user can use this link to make an account. Once the person is registered, you can accept (or reject) him at the **Requests** section (see below). + +1. Type the email address of the person you want to invite in the input field. +2. Click the **Send invite** button to create an email to the given address OR Use the dropdown of the button and click **Copy invite link** to copy the link. You can choose via which way you provide the new user the invite link. + +#### Requests + +At the top middle of the page, you find a dropdown with **Requests**. When you expend the requests, you see a list of all user requests. This are all users who used an invite link to create an account. + +Note: the list only contains requests from the current selected edition. Each edition has his own requests. + +The list can be filtered by searching a name. Each row contains the name and email 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. + +#### Coaches + +A the centre of the page, you find a list of all users who are coach in the current edition. As in the request list, each row contains the name and email address of a user. The list can be filtered by searching a 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 a coach, 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. + +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. + +### Admins + +This page consists of a list of all users who are admin. An admin can see all editions and change all data (including deleting a whole edition). Each row in the list contains the name and email (including register-method) of every admin. The list can be filtered by name via the input field. + +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. + +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 an remove other admins. From 75811b50bb6743486cca076bc5297354b03e798f Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Fri, 22 Apr 2022 16:38:43 +0200 Subject: [PATCH 446/536] use the correct editionId instead of hardcoding the id --- backend/migrations/versions/64c42bb48aee_.py | 24 +++++++++++++++++++ .../ProjectCard/ProjectCard.tsx | 10 ++++++-- .../ProjectDetailPage/ProjectDetailPage.tsx | 7 +++--- .../ProjectsPage/ProjectsPage.tsx | 8 +++++-- 4 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 backend/migrations/versions/64c42bb48aee_.py diff --git a/backend/migrations/versions/64c42bb48aee_.py b/backend/migrations/versions/64c42bb48aee_.py new file mode 100644 index 000000000..23597345c --- /dev/null +++ b/backend/migrations/versions/64c42bb48aee_.py @@ -0,0 +1,24 @@ +"""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/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 0441a853c..a24c68260 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -20,7 +20,7 @@ import { useState } from "react"; import ConfirmDelete from "../ConfirmDelete"; import { deleteProject } from "../../../utils/api/projects"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { Project } from "../../../data/interfaces"; @@ -50,11 +50,17 @@ export default function ProjectCard({ }; const navigate = useNavigate(); + const params = useParams(); + const editionId = params.editionId!; return ( - navigate("/editions/2022/projects/" + project.projectId)}> + <Title + onClick={() => + navigate("/editions/" + editionId + "/projects/" + project.projectId) + } + > {project.name} <OpenIcon /> diff --git a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx index 441c0f680..9c0184eef 100644 --- a/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx +++ b/frontend/src/views/projectViews/ProjectDetailPage/ProjectDetailPage.tsx @@ -29,6 +29,7 @@ import { export default function ProjectDetailPage() { const params = useParams(); const projectId = parseInt(params.projectId!); + const editionId = params.editionId!; const [project, setProject] = useState(); const [gotProject, setGotProject] = useState(false); @@ -41,7 +42,7 @@ export default function ProjectDetailPage() { async function callProjects() { if (projectId) { setGotProject(true); - const response = await getProject("2022", projectId); + const response = await getProject(editionId, projectId); if (response) { setProject(response); @@ -63,14 +64,14 @@ export default function ProjectDetailPage() { if (!gotProject) { callProjects(); } - }, [gotProject, navigate, projectId]); + }, [editionId, gotProject, navigate, projectId]); if (!project) return null; return (
                                    - navigate("/editions/2022/projects/")}> + navigate("/editions/" + editionId + "/projects/")}> Overview diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index 305c53777..c88cd676f 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -4,6 +4,7 @@ import { ProjectCard } from "../../../components/ProjectsComponents"; import { CardsGrid, CreateButton, SearchButton, SearchField, OwnProject } from "./styles"; import { useAuth } from "../../../contexts/auth-context"; import { Project } from "../../../data/interfaces"; +import { useParams } from "react-router-dom"; /** * @returns The projects overview page where you can see all the projects. @@ -22,6 +23,9 @@ export default function ProjectPage() { const { userId } = useAuth(); + const params = useParams(); + const editionId = params.editionId!; + /** * Uses to filter the results based onto search string and own projects */ @@ -51,7 +55,7 @@ export default function ProjectPage() { useEffect(() => { async function callProjects() { setGotProjects(true); - const response = await getProjects("2022"); + const response = await getProjects(editionId); if (response) { setProjectsAPI(response.projects); setProjects(response.projects); @@ -60,7 +64,7 @@ export default function ProjectPage() { if (!gotProjects) { callProjects(); } - }, [gotProjects]); + }, [editionId, gotProjects]); return (
                                    From a6a9592bc2d8c28c30fa80ce587da37531ec1610 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Fri, 22 Apr 2022 20:02:51 +0200 Subject: [PATCH 447/536] started working on pagination for projects --- frontend/src/utils/api/projects.ts | 4 ++-- .../projectViews/ProjectsPage/ProjectsPage.tsx | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index e49016530..8c806a330 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -2,9 +2,9 @@ import axios from "axios"; import { Projects, Project } from "../../data/interfaces/projects"; import { axiosInstance } from "./api"; -export async function getProjects(edition: string) { +export async function getProjects(edition: string, page: number) { try { - const response = await axiosInstance.get("/editions/" + edition + "/projects/"); + const response = await axiosInstance.get("/editions/" + edition + "/projects?page=" + page.toString()); const projects = response.data as Projects; return projects; } catch (error) { diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index c88cd676f..24504295c 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -5,6 +5,7 @@ import { CardsGrid, CreateButton, SearchButton, SearchField, OwnProject } from " import { useAuth } from "../../../contexts/auth-context"; import { Project } from "../../../data/interfaces"; import { useParams } from "react-router-dom"; +import { Button } from "react-bootstrap"; /** * @returns The projects overview page where you can see all the projects. @@ -21,6 +22,8 @@ export default function ProjectPage() { const [searchString, setSearchString] = useState(""); const [ownProjects, setOwnProjects] = useState(false); + const [page, setPage] = useState(0) + const { userId } = useAuth(); const params = useParams(); @@ -55,16 +58,15 @@ export default function ProjectPage() { useEffect(() => { async function callProjects() { setGotProjects(true); - const response = await getProjects(editionId); + const response = await getProjects(editionId, page); if (response) { setProjectsAPI(response.projects); setProjects(response.projects); } } - if (!gotProjects) { - callProjects(); - } - }, [editionId, gotProjects]); + callProjects(); + + }, [editionId, gotProjects, page]); return (
                                    @@ -96,6 +98,8 @@ export default function ProjectPage() { /> ))} + +
                                    ); } From e256c33503aeee2df73f9798c32326e377cc4896 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 22 Apr 2022 22:28:20 +0200 Subject: [PATCH 448/536] changed return type --- backend/src/app/logic/students.py | 20 ++++++++++++++----- .../app/routers/editions/students/students.py | 5 +++-- backend/src/app/schemas/students.py | 7 ++++++- .../test_students/test_students.py | 9 ++++++++- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index 96be35ba0..2c0d87479 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -14,7 +14,8 @@ from src.app.schemas.students import ( ReturnStudentList, ReturnStudent, CommonQueryParams, ReturnStudentMailList, Student as StudentModel, Suggestions as SuggestionsModel, - NewEmail, DecisionEmail as DecionEmailModel, EmailsSearchQueryParams) + NewEmail, DecisionEmail as DecionEmailModel, EmailsSearchQueryParams, + ListReturnStudentMailList) def definitive_decision_on_student(db: Session, student: Student, decision: NewDecision) -> None: @@ -75,7 +76,7 @@ def get_emails_of_student(db: Session, edition: Edition, student: Student) -> Re if student.edition != edition: raise NoResultFound emails: list[DecisionEmail] = get_emails(db, student) - return ReturnStudentMailList(emails=emails) + return ReturnStudentMailList(emails=emails, student=student) def make_new_email(db: Session, edition: Edition, new_email: NewEmail) -> DecionEmailModel: @@ -87,7 +88,16 @@ def make_new_email(db: Session, edition: Edition, new_email: NewEmail) -> Decion return email -def last_emails_of_students(db: Session, edition: Edition, commons: EmailsSearchQueryParams) -> ReturnStudentMailList: +def last_emails_of_students(db: Session, edition: Edition, + commons: EmailsSearchQueryParams) -> ListReturnStudentMailList: """get last emails of students with search params""" - emails: list[DecisionEmail] = get_last_emails_of_students(db, edition, commons) - return ReturnStudentMailList(emails=emails) + emails: list[DecisionEmail] = get_last_emails_of_students( + db, edition, commons) + student_emails: list[ReturnStudentMailList] = [] + for email in emails: + student=get_student_by_id(db, email.student_id) + student_emails.append(ReturnStudentMailList(student=student, + emails=[DecisionEmail(email_id=email.email_id, + decision=email.decision, + date=email.date)])) + return ListReturnStudentMailList(student_emails=student_emails) diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index 034c31ea9..1617ce9ec 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -9,7 +9,8 @@ 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, DecisionEmail, NewEmail, EmailsSearchQueryParams) + ReturnStudentMailList, DecisionEmail, NewEmail, EmailsSearchQueryParams, + ListReturnStudentMailList) from src.database.database import get_session from src.database.models import Student, Edition from .suggestions import students_suggestions_router @@ -39,7 +40,7 @@ async def send_emails(new_email: NewEmail, db: Session = Depends(get_session), e @students_router.get("/emails", dependencies=[Depends(require_admin)], - response_model=ReturnStudentMailList) + response_model=ListReturnStudentMailList) async def get_emails(db: Session = Depends(get_session), edition: Edition = Depends(get_edition), commons: EmailsSearchQueryParams = Depends(EmailsSearchQueryParams)): """ diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index 0842bb92e..a8633666c 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -86,7 +86,6 @@ class DecisionEmail(CamelCaseModel): Model to represent DecisionEmail """ email_id: int - student: Student decision: EmailStatusEnum date: datetime @@ -99,9 +98,15 @@ class ReturnStudentMailList(CamelCaseModel): """ Model to return a list of mails of a student """ + student: Student emails: list[DecisionEmail] +class ListReturnStudentMailList(CamelCaseModel): + """Model to give a list of ReturnStudentMailList""" + student_emails: list[ReturnStudentMailList] + + class NewEmail(CamelCaseModel): """The fields of a DecisionEmail""" student_id: int 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 bb3c62656..b1bf355a2 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 @@ -397,9 +397,11 @@ def test_get_emails_student_admin(database_with_data: Session, auth_client: Auth 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): @@ -521,5 +523,10 @@ def test_get_emails(database_with_data: Session, auth_client: AuthClient): auth_client.post("/editions/ed2022/students/emails", json={"student_id": 2, "email_status": 5}) response = auth_client.get("/editions/ed2022/students/emails") + print(response.json()) assert response.status_code == status.HTTP_200_OK - assert len(response.json()["emails"]) == 2 + assert len(response.json()["studentEmails"]) == 2 + assert response.json()["studentEmails"][0]["student"]["studentId"] == 1 + assert response.json()["studentEmails"][0]["emails"][0]["decision"] == 3 + assert response.json()["studentEmails"][1]["student"]["studentId"] == 2 + assert response.json()["studentEmails"][1]["emails"][0]["decision"] == 5 From a9ce1aa634de58205a565b3b1ded98653a20f82e Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Fri, 22 Apr 2022 23:30:28 +0200 Subject: [PATCH 449/536] you can send a list of students to send mails --- backend/src/app/logic/students.py | 21 +++++----- .../app/routers/editions/students/students.py | 2 +- backend/src/app/schemas/students.py | 2 +- .../test_students/test_students.py | 40 ++++++++++--------- 4 files changed, 35 insertions(+), 30 deletions(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index 2c0d87479..aa3381cc0 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -1,3 +1,4 @@ +from re import S from sqlalchemy.orm import Session from sqlalchemy.orm.exc import NoResultFound @@ -79,13 +80,17 @@ def get_emails_of_student(db: Session, edition: Edition, student: Student) -> Re return ReturnStudentMailList(emails=emails, student=student) -def make_new_email(db: Session, edition: Edition, new_email: NewEmail) -> DecionEmailModel: +def make_new_email(db: Session, edition: Edition, new_email: NewEmail) -> ListReturnStudentMailList: """make a new email""" - student = get_student_by_id(db, new_email.student_id) - if student.edition != edition: - raise FailedToAddNewEmailException - email: DecisionEmail = create_email(db, student, new_email.email_status) - return email + student_emails: list[ReturnStudentMailList] = [] + for student_id in new_email.students_id: + student: Student = get_student_by_id(db, student_id) + if student.edition == edition: + email: DecisionEmail = 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, @@ -97,7 +102,5 @@ def last_emails_of_students(db: Session, edition: Edition, for email in emails: student=get_student_by_id(db, email.student_id) student_emails.append(ReturnStudentMailList(student=student, - emails=[DecisionEmail(email_id=email.email_id, - decision=email.decision, - date=email.date)])) + emails=[email])) return ListReturnStudentMailList(student_emails=student_emails) diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index 1617ce9ec..323d99743 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -31,7 +31,7 @@ async def get_students(db: Session = Depends(get_session), @students_router.post("/emails", dependencies=[Depends(require_admin)], - status_code=status.HTTP_201_CREATED, response_model=DecisionEmail) + 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)): """ Send a email to a list of students. diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index a8633666c..fcbb5ffdf 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -109,5 +109,5 @@ class ListReturnStudentMailList(CamelCaseModel): class NewEmail(CamelCaseModel): """The fields of a DecisionEmail""" - student_id: int + students_id: list[int] email_status: EmailStatusEnum 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 b1bf355a2..602594f4a 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 @@ -422,67 +422,68 @@ def test_post_email_applied(database_with_data: Session, auth_client: AuthClient """test create email applied""" auth_client.admin() response = auth_client.post("/editions/ed2022/students/emails", - json={"student_id": 2, "email_status": 0}) + json={"students_id": [2], "email_status": 0}) assert response.status_code == status.HTTP_201_CREATED assert EmailStatusEnum( - response.json()["decision"]) == EmailStatusEnum.APPLIED + response.json()["studentEmails"][0]["emails"][0]["decision"]) == EmailStatusEnum.APPLIED def test_post_email_awaiting_project(database_with_data: Session, auth_client: AuthClient): """test create email awaiting project""" auth_client.admin() response = auth_client.post("/editions/ed2022/students/emails", - json={"student_id": 2, "email_status": 1}) + json={"students_id": [2], "email_status": 1}) assert response.status_code == status.HTTP_201_CREATED assert EmailStatusEnum( - response.json()["decision"]) == EmailStatusEnum.AWAITING_PROJECT + response.json()["studentEmails"][0]["emails"][0]["decision"]) == EmailStatusEnum.AWAITING_PROJECT def test_post_email_approved(database_with_data: Session, auth_client: AuthClient): """test create email applied""" auth_client.admin() response = auth_client.post("/editions/ed2022/students/emails", - json={"student_id": 2, "email_status": 2}) + json={"students_id": [2], "email_status": 2}) assert response.status_code == status.HTTP_201_CREATED assert EmailStatusEnum( - response.json()["decision"]) == EmailStatusEnum.APPROVED + response.json()["studentEmails"][0]["emails"][0]["decision"]) == EmailStatusEnum.APPROVED def test_post_email_contract_confirmed(database_with_data: Session, auth_client: AuthClient): """test create email contract confirmed""" auth_client.admin() response = auth_client.post("/editions/ed2022/students/emails", - json={"student_id": 2, "email_status": 3}) + json={"students_id": [2], "email_status": 3}) assert response.status_code == status.HTTP_201_CREATED assert EmailStatusEnum( - response.json()["decision"]) == EmailStatusEnum.CONTRACT_CONFIRMED + response.json()["studentEmails"][0]["emails"][0]["decision"]) == EmailStatusEnum.CONTRACT_CONFIRMED def test_post_email_contract_declined(database_with_data: Session, auth_client: AuthClient): """test create email contract declined""" auth_client.admin() response = auth_client.post("/editions/ed2022/students/emails", - json={"student_id": 2, "email_status": 4}) + json={"students_id": [2], "email_status": 4}) assert response.status_code == status.HTTP_201_CREATED assert EmailStatusEnum( - response.json()["decision"]) == EmailStatusEnum.CONTRACT_DECLINED + response.json()["studentEmails"][0]["emails"][0]["decision"]) == EmailStatusEnum.CONTRACT_DECLINED def test_post_email_rejected(database_with_data: Session, auth_client: AuthClient): """test create email rejected""" auth_client.admin() response = auth_client.post("/editions/ed2022/students/emails", - json={"student_id": 2, "email_status": 5}) + json={"students_id": [2], "email_status": 5}) assert response.status_code == status.HTTP_201_CREATED + print(response.json()) assert EmailStatusEnum( - response.json()["decision"]) == EmailStatusEnum.REJECTED + response.json()["studentEmails"][0]["emails"][0]["decision"]) == EmailStatusEnum.REJECTED def test_creat_email_for_ghost(database_with_data: Session, auth_client: AuthClient): """test create email for student that don't exist""" auth_client.admin() response = auth_client.post("/editions/ed2022/students/emails", - json={"student_id": 100, "email_status": 5}) + json={"students_id": [100], "email_status": 5}) assert response.status_code == status.HTTP_404_NOT_FOUND @@ -497,8 +498,10 @@ def test_creat_email_student_in_other_edition(database_with_data: Session, auth_ database_with_data.commit() auth_client.admin() response = auth_client.post("/editions/ed2022/students/emails", - json={"student_id": 3, "email_status": 5}) - assert response.status_code == status.HTTP_400_BAD_REQUEST + json={"students_id": [3], "email_status": 5}) + print(response.json()) + assert response.status_code == status.HTTP_201_CREATED + assert len(response.json()["studentEmails"]) == 0 def test_get_emails_no_authorization(database_with_data: Session, auth_client: AuthClient): @@ -518,12 +521,11 @@ def test_get_emails_coach(database_with_data: Session, auth_client: AuthClient): def test_get_emails(database_with_data: Session, 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={"student_id": 1, "email_status": 3}) - auth_client.post("/editions/ed2022/students/emails", - json={"student_id": 2, "email_status": 5}) + json={"students_id": [2], "email_status": 5}) response = auth_client.get("/editions/ed2022/students/emails") - print(response.json()) assert response.status_code == status.HTTP_200_OK assert len(response.json()["studentEmails"]) == 2 assert response.json()["studentEmails"][0]["student"]["studentId"] == 1 From eb0dc35d1d3f6d96773b50b9830bb56877b24618 Mon Sep 17 00:00:00 2001 From: Francis Date: Sat, 23 Apr 2022 10:13:23 +0200 Subject: [PATCH 450/536] remove self-hosted requirement --- .github/workflows/backend.yml | 4 ---- .github/workflows/frontend.yml | 5 ----- 2 files changed, 9 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 2be992c0f..fa480684f 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -12,7 +12,6 @@ defaults: jobs: Dependencies: - runs-on: self-hosted container: python:3.10.2-slim-bullseye steps: - uses: actions/checkout@v2 @@ -40,7 +39,6 @@ jobs: Test: needs: [Dependencies] - runs-on: self-hosted container: python:3.10.2-slim-bullseye steps: - uses: actions/checkout@v2 @@ -53,7 +51,6 @@ jobs: Lint: needs: [Test] - runs-on: self-hosted container: python:3.10.2-slim-bullseye steps: - uses: actions/checkout@v2 @@ -66,7 +63,6 @@ jobs: Type: needs: [Test] - runs-on: self-hosted container: python:3.10.2-slim-bullseye steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index d93d40559..b080f5763 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -12,7 +12,6 @@ defaults: jobs: Dependencies: - runs-on: self-hosted container: node:16.14.0-alpine steps: - run: apk add --no-cache tar @@ -35,7 +34,6 @@ jobs: Test: needs: [Dependencies] - runs-on: self-hosted container: node:16.14.0-alpine steps: - run: apk add --no-cache tar @@ -55,7 +53,6 @@ jobs: Lint: needs: [Test] - runs-on: self-hosted container: node:16.14.0-alpine steps: - run: apk add --no-cache tar @@ -75,7 +72,6 @@ jobs: Style: needs: [Test] - runs-on: self-hosted container: node:16.14.0-alpine steps: - run: apk add --no-cache tar @@ -95,7 +91,6 @@ jobs: Build: needs: [Style, Lint] - runs-on: self-hosted container: node:16.14.0-alpine steps: - run: apk add --no-cache tar From 0645727e0dc9a9f1de69c0532375e16428ac97c5 Mon Sep 17 00:00:00 2001 From: Francis Date: Sat, 23 Apr 2022 13:33:23 +0200 Subject: [PATCH 451/536] remove leftover file --- frontend/src/components/navbar/NavBar.tsx | 45 ----------------------- 1 file changed, 45 deletions(-) delete mode 100644 frontend/src/components/navbar/NavBar.tsx diff --git a/frontend/src/components/navbar/NavBar.tsx b/frontend/src/components/navbar/NavBar.tsx deleted file mode 100644 index 04ac277ed..000000000 --- a/frontend/src/components/navbar/NavBar.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from "react"; -import { Bars, Nav, NavLink, NavMenu } from "./NavBarElements"; -import "./navbar.css"; -import { useAuth } from "../../contexts"; - -/** - * NavBar displayed at the top of the page. - * Links are hidden if the user is not authorized to see them. - */ -export default function NavBar() { - const { isLoggedIn } = useAuth(); - const hidden = isLoggedIn ? "nav-links" : "nav-hidden"; - - return ( - <> - - - ); -} From 3601cd35b5917b0d2208939801ac297f4ee9d87a Mon Sep 17 00:00:00 2001 From: Francis Date: Sat, 23 Apr 2022 13:41:22 +0200 Subject: [PATCH 452/536] missed a conflict --- backend/src/app/exceptions/handlers.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/src/app/exceptions/handlers.py b/backend/src/app/exceptions/handlers.py index 6a9a18e34..afa50b5ab 100644 --- a/backend/src/app/exceptions/handlers.py +++ b/backend/src/app/exceptions/handlers.py @@ -97,18 +97,15 @@ def failed_to_add_project_role_exception(_request: Request, _exception: FailedTo content={'message': 'Something went wrong while adding this student to the project'} ) -<<<<<<< HEAD @app.exception_handler(WrongTokenTypeException) async def wrong_token_type_exception(_request: Request, _exception: WrongTokenTypeException): return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, content={'message': 'U used the wrong token to access this resource.'} ) -======= @app.exception_handler(ReadOnlyEditionException) def read_only_edition_exception(_request: Request, _exception: ReadOnlyEditionException): return JSONResponse( status_code=status.HTTP_405_METHOD_NOT_ALLOWED, content={'message': 'This edition is Read-Only'} ) ->>>>>>> develop From ad8c61e4ccac4a5be929fbeb4c139d9a330d235b Mon Sep 17 00:00:00 2001 From: Francis Date: Sat, 23 Apr 2022 14:01:27 +0200 Subject: [PATCH 453/536] fix frontend tests --- frontend/src/setupTests.ts | 8 ++++++++ frontend/src/tests/utils/contexts.tsx | 2 -- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts index e4fe2045d..2bde9417f 100644 --- a/frontend/src/setupTests.ts +++ b/frontend/src/setupTests.ts @@ -17,6 +17,14 @@ jest.mock("axios", () => { defaults: { baseURL: "", }, + interceptors: { + request: { + use: jest.fn() + }, + response: { + use: jest.fn() + } + }, get: jest.fn(), post: jest.fn(), }; diff --git a/frontend/src/tests/utils/contexts.tsx b/frontend/src/tests/utils/contexts.tsx index 06e65a187..6c6ba2aae 100644 --- a/frontend/src/tests/utils/contexts.tsx +++ b/frontend/src/tests/utils/contexts.tsx @@ -13,8 +13,6 @@ export function defaultAuthState(): AuthContextState { setRole: jest.fn(), userId: null, setUserId: jest.fn(), - token: null, - setToken: jest.fn(), editions: [], setEditions: jest.fn(), }; From 4f7b66589177d633fc0e790db94fb2d9833fc1e6 Mon Sep 17 00:00:00 2001 From: Francis Date: Sat, 23 Apr 2022 14:01:53 +0200 Subject: [PATCH 454/536] add tool-versions for node --- frontend/.tool-versions | 1 + 1 file changed, 1 insertion(+) create mode 100644 frontend/.tool-versions diff --git a/frontend/.tool-versions b/frontend/.tool-versions new file mode 100644 index 000000000..009455657 --- /dev/null +++ b/frontend/.tool-versions @@ -0,0 +1 @@ +nodejs 16.14.2 From 85f59ea757213c06e25bf3b0cfa44a8693794da2 Mon Sep 17 00:00:00 2001 From: Francis Date: Sat, 23 Apr 2022 14:11:02 +0200 Subject: [PATCH 455/536] fix backend linting --- backend/settings.py | 4 ++-- backend/src/app/exceptions/authentication.py | 2 +- backend/src/app/logic/security.py | 4 ++-- backend/src/app/routers/login/login.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/settings.py b/backend/settings.py index a37091f90..e07ad6474 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -30,8 +30,8 @@ """JWT token key""" SECRET_KEY: str = env.str("SECRET_KEY", "4d16a9cc83d74144322e893c879b5f639088c15dc1606b11226abbd7e97f5ee5") -ACCESS_TOKEN_EXPIRE_MINUTES: int = env.int("ACCESS_TOKEN_EXPIRE_MINUTES", 1) -REFRESH_TOKEN_EXPIRE_MINUTES: int = env.int("REFRESH_TOKEN_EXPIRE_MINUTES", 2) +ACCESS_TOKEN_EXPIRE_M: int = env.int("ACCESS_TOKEN_EXPIRE_M", 1) +REFRESH_TOKEN_EXPIRE_M: int = env.int("REFRESH_TOKEN_EXPIRE_M", 2) """Frontend""" FRONTEND_URL: str = env.str("FRONTEND_URL", "http://localhost:3000") diff --git a/backend/src/app/exceptions/authentication.py b/backend/src/app/exceptions/authentication.py index f7bf55815..1445eecf8 100644 --- a/backend/src/app/exceptions/authentication.py +++ b/backend/src/app/exceptions/authentication.py @@ -29,4 +29,4 @@ class WrongTokenTypeException(ValueError): Exception raised when a request to a private route is made with a valid jwt token, but a wrong token type. eg: trying to authenticate using a refresh token - """ \ No newline at end of file + """ diff --git a/backend/src/app/logic/security.py b/backend/src/app/logic/security.py index 73d48b417..7191d9350 100644 --- a/backend/src/app/logic/security.py +++ b/backend/src/app/logic/security.py @@ -31,8 +31,8 @@ def create_tokens(user: User) -> tuple[str, str]: Returns: (access_token, refresh_token) """ return ( - _create_token({"type": TokenType.ACCESS.value, "sub": str(user.user_id)}, settings.ACCESS_TOKEN_EXPIRE_MINUTES), - _create_token({"type": TokenType.REFRESH.value, "sub": str(user.user_id)}, settings.REFRESH_TOKEN_EXPIRE_MINUTES) + _create_token({"type": TokenType.ACCESS.value, "sub": str(user.user_id)}, settings.ACCESS_TOKEN_EXPIRE_M), + _create_token({"type": TokenType.REFRESH.value, "sub": str(user.user_id)}, settings.REFRESH_TOKEN_EXPIRE_M) ) diff --git a/backend/src/app/routers/login/login.py b/backend/src/app/routers/login/login.py index 0dbc63da0..303400937 100644 --- a/backend/src/app/routers/login/login.py +++ b/backend/src/app/routers/login/login.py @@ -10,7 +10,7 @@ from src.app.routers.tags import Tags from src.app.schemas.login import Token, UserData from src.app.schemas.users import user_model_to_schema -from src.app.utils.dependencies import get_current_active_user, get_user_from_refresh_token +from src.app.utils.dependencies import get_user_from_refresh_token from src.database.database import get_session from src.database.models import User From 19484c7578bf884dc0d4b3f50ade25668a231270 Mon Sep 17 00:00:00 2001 From: Francis Date: Sat, 23 Apr 2022 14:23:33 +0200 Subject: [PATCH 456/536] fix pylint warning --- backend/src/app/logic/security.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/app/logic/security.py b/backend/src/app/logic/security.py index 7191d9350..22fd39d60 100644 --- a/backend/src/app/logic/security.py +++ b/backend/src/app/logic/security.py @@ -20,6 +20,7 @@ @enum.unique class TokenType(enum.Enum): + """Type of the token, used to check no access token is used to refresh and the reverse.""" ACCESS = "access" REFRESH = "refresh" From 83537366b238996ffec3294a4c1e68e0aef83e54 Mon Sep 17 00:00:00 2001 From: Francis Date: Sat, 23 Apr 2022 14:32:04 +0200 Subject: [PATCH 457/536] run prettier --- frontend/src/setupTests.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts index 2bde9417f..18cccfe12 100644 --- a/frontend/src/setupTests.ts +++ b/frontend/src/setupTests.ts @@ -19,11 +19,11 @@ jest.mock("axios", () => { }, interceptors: { request: { - use: jest.fn() + use: jest.fn(), }, response: { - use: jest.fn() - } + use: jest.fn(), + }, }, get: jest.fn(), post: jest.fn(), From 63784ea5b15107fbf7022c19d78b3a1db53a6dd1 Mon Sep 17 00:00:00 2001 From: Francis Date: Sat, 23 Apr 2022 17:04:29 +0200 Subject: [PATCH 458/536] set reasonable token exiry --- backend/.env.example | 4 ++-- backend/settings.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index 2836e638d..eb6a08b2d 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -8,9 +8,9 @@ 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_MINUTES = 1 +ACCESS_TOKEN_EXPIRE_M = 5 # The REFRESH JWT token should be valid for ... -REFRESH_TOKEN_EXPIRE_MINUTES = 2 +REFRESH_TOKEN_EXPIRE_M = 2880 # Frontend FRONTEND_URL="http://localhost:3000" \ No newline at end of file diff --git a/backend/settings.py b/backend/settings.py index e07ad6474..342f8bd35 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -30,8 +30,8 @@ """JWT token key""" SECRET_KEY: str = env.str("SECRET_KEY", "4d16a9cc83d74144322e893c879b5f639088c15dc1606b11226abbd7e97f5ee5") -ACCESS_TOKEN_EXPIRE_M: int = env.int("ACCESS_TOKEN_EXPIRE_M", 1) -REFRESH_TOKEN_EXPIRE_M: int = env.int("REFRESH_TOKEN_EXPIRE_M", 2) +ACCESS_TOKEN_EXPIRE_M: int = env.int("ACCESS_TOKEN_EXPIRE_M", 5) +REFRESH_TOKEN_EXPIRE_M: int = env.int("REFRESH_TOKEN_EXPIRE_M", 2880) """Frontend""" FRONTEND_URL: str = env.str("FRONTEND_URL", "http://localhost:3000") From 4c173499435d860b772a924fdba8e0385b57ea41 Mon Sep 17 00:00:00 2001 From: Francis Date: Sat, 23 Apr 2022 17:09:14 +0200 Subject: [PATCH 459/536] change build trigger --- .github/workflows/backend.yml | 8 ++++---- .github/workflows/frontend.yml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index fa480684f..6df81b11c 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -1,10 +1,10 @@ name: Backend CI on: + pull_request: push: - paths: - - ".github/workflows/backend.yml" - - "backend/**" - - "!**.md" + branches: + - master + - develop defaults: run: diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index b080f5763..b516e62a6 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -1,10 +1,10 @@ name: Frontend CI on: + pull_request: push: - paths: - - ".github/workflows/frontend.yml" - - "frontend/**" - - "!**.md" + branches: + - master + - develop defaults: run: From 6e0f503e5f50b774de9ab0526c326eceed464311 Mon Sep 17 00:00:00 2001 From: Francis Date: Sat, 23 Apr 2022 17:14:49 +0200 Subject: [PATCH 460/536] add required runs-on --- .github/workflows/backend.yml | 4 ++++ .github/workflows/frontend.yml | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 6df81b11c..16ebf7272 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -12,6 +12,7 @@ defaults: jobs: Dependencies: + runs-on: ubuntu-latest container: python:3.10.2-slim-bullseye steps: - uses: actions/checkout@v2 @@ -39,6 +40,7 @@ jobs: Test: needs: [Dependencies] + runs-on: ubuntu-latest container: python:3.10.2-slim-bullseye steps: - uses: actions/checkout@v2 @@ -51,6 +53,7 @@ jobs: Lint: needs: [Test] + runs-on: ubuntu-latest container: python:3.10.2-slim-bullseye steps: - uses: actions/checkout@v2 @@ -63,6 +66,7 @@ jobs: Type: needs: [Test] + runs-on: ubuntu-latest container: python:3.10.2-slim-bullseye steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index b516e62a6..47ce6b72f 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -12,6 +12,7 @@ defaults: jobs: Dependencies: + runs-on: ubuntu-latest container: node:16.14.0-alpine steps: - run: apk add --no-cache tar @@ -34,6 +35,7 @@ jobs: Test: needs: [Dependencies] + runs-on: ubuntu-latest container: node:16.14.0-alpine steps: - run: apk add --no-cache tar @@ -53,6 +55,7 @@ jobs: Lint: needs: [Test] + runs-on: ubuntu-latest container: node:16.14.0-alpine steps: - run: apk add --no-cache tar @@ -72,6 +75,7 @@ jobs: Style: needs: [Test] + runs-on: ubuntu-latest container: node:16.14.0-alpine steps: - run: apk add --no-cache tar @@ -91,6 +95,7 @@ jobs: Build: needs: [Style, Lint] + runs-on: ubuntu-latest container: node:16.14.0-alpine steps: - run: apk add --no-cache tar From 54c0610a29362c4ea620d4ddf0118ded5924c77d Mon Sep 17 00:00:00 2001 From: Francis Date: Sat, 23 Apr 2022 17:28:11 +0200 Subject: [PATCH 461/536] use different container --- .github/workflows/frontend.yml | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 47ce6b72f..1e9a778bb 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -13,11 +13,9 @@ defaults: jobs: Dependencies: runs-on: ubuntu-latest - container: node:16.14.0-alpine + container: node:16.14.0-bullseye-slim steps: - - run: apk add --no-cache tar - uses: actions/checkout@v2 - - name: Get yarn cache directory path id: yarn-cache-dir run: echo "::set-output name=dir::$(yarn cache dir)" @@ -36,11 +34,9 @@ jobs: Test: needs: [Dependencies] runs-on: ubuntu-latest - container: node:16.14.0-alpine + container: node:16.14.0-bullseye-slim steps: - - run: apk add --no-cache tar - uses: actions/checkout@v2 - - name: Get yarn cache directory path id: yarn-cache-dir run: echo "::set-output name=dir::$(yarn cache dir)" @@ -56,11 +52,9 @@ jobs: Lint: needs: [Test] runs-on: ubuntu-latest - container: node:16.14.0-alpine + container: node:16.14.0-bullseye-slim steps: - - run: apk add --no-cache tar - uses: actions/checkout@v2 - - name: Get yarn cache directory path id: yarn-cache-dir run: echo "::set-output name=dir::$(yarn cache dir)" @@ -76,11 +70,9 @@ jobs: Style: needs: [Test] runs-on: ubuntu-latest - container: node:16.14.0-alpine + container: node:16.14.0-bullseye-slim steps: - - run: apk add --no-cache tar - uses: actions/checkout@v2 - - name: Get yarn cache directory path id: yarn-cache-dir run: echo "::set-output name=dir::$(yarn cache dir)" @@ -96,11 +88,9 @@ jobs: Build: needs: [Style, Lint] runs-on: ubuntu-latest - container: node:16.14.0-alpine + container: node:16.14.0-bullseye-slim steps: - - run: apk add --no-cache tar - uses: actions/checkout@v2 - - name: Get yarn cache directory path id: yarn-cache-dir run: echo "::set-output name=dir::$(yarn cache dir)" From 84a444cdbb5b4f62862325c7225a0765896bbb44 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sat, 23 Apr 2022 18:15:24 +0200 Subject: [PATCH 462/536] Create edition page --- frontend/src/Router.tsx | 2 +- frontend/src/utils/api/editions.ts | 23 ++++ .../CreateEditionPage/CreateEditionPage.tsx | 127 +++++++++++++++++- .../src/views/CreateEditionPage/styles.ts | 24 ++++ frontend/src/views/index.ts | 1 + 5 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 frontend/src/views/CreateEditionPage/styles.ts diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index c3d8f31ac..7a0a208a5 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -5,6 +5,7 @@ import { AdminRoute, Footer, Navbar, PrivateRoute } from "./components"; import { useAuth } from "./contexts"; import { EditionsPage, + CreateEditionPage, LoginPage, PendingPage, ProjectsPage, @@ -14,7 +15,6 @@ import { VerifyingTokenPage, } from "./views"; import { ForbiddenPage, NotFoundPage } from "./views/errors"; -import CreateEditionPage from "./views/CreateEditionPage"; import { Role } from "./data/enums"; /** diff --git a/frontend/src/utils/api/editions.ts b/frontend/src/utils/api/editions.ts index ad05a3515..f85e297f8 100644 --- a/frontend/src/utils/api/editions.ts +++ b/frontend/src/utils/api/editions.ts @@ -1,10 +1,16 @@ import { axiosInstance } from "./api"; import { Edition } from "../../data/interfaces"; +import axios from "axios"; interface EditionsResponse { editions: Edition[]; } +interface EditionFields { + name: string; + year: number; +} + /** * Get all editions the user can see. */ @@ -20,3 +26,20 @@ export async function deleteEdition(name: string): Promise { const response = await axiosInstance.delete(`/editions/${name}`); return response.status; } + +/** + * 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 }; + try { + const response = await axiosInstance.post("/editions/", payload); + return response.status; + } catch (error) { + if (axios.isAxiosError(error) && error.response !== undefined) { + return error.response.status; + } else { + return -1; + } + } +} diff --git a/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx b/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx index 93fb5bff7..4f0d9651f 100644 --- a/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx +++ b/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx @@ -1,6 +1,129 @@ +import { Button, Form, Spinner } from "react-bootstrap"; +import { SyntheticEvent, useState } from "react"; +import { createEdition } from "../../utils/api/editions"; +import { useNavigate } from "react-router-dom"; +import { CreateEditionDiv, Error, FormGroup, ButtonDiv } from "./styles"; + /** - * Page to create a new edition + * Page to create a new edition. */ export default function CreateEditionPage() { - return
                                    Here be create edition form
                                    ; + const navigate = useNavigate(); + const currentYear = new Date().getFullYear(); + + 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 [loading, setLoading] = useState(false); + + async function sendEdition(name: string, year: number) { + const response = await createEdition(name, year); + setLoading(false); + let success = false; + if (response === 201) { + success = true; + } else if (response === 409) { + setNameError("Edition name already exists."); + } else if (response === 422) { + setNameError("Invalid edition name."); + } else { + setError("Something went wrong."); + } + if (success) { + // navigate must be at the end of the function + navigate("/editions/"); + } + } + + const handleSubmit = (event: SyntheticEvent) => { + 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."); + } else { + setNameError("Invalid edition name."); + } + correct = false; + } + + const yearNumber = Number(year); + if (isNaN(yearNumber)) { + correct = false; + setYearError("Invalid year."); + } else { + if (yearNumber < currentYear) { + correct = false; + setYearError("New editions can't be in the past."); + } else if (yearNumber > 3000) { + correct = false; + setYearError("Invalid year."); + } + } + + if (correct) { + setLoading(true); + sendEdition(name, yearNumber); + } + event.preventDefault(); + event.stopPropagation(); + }; + + let submitButton; + if (loading) { + submitButton = ; + } else { + submitButton = ( + + ); + } + + return ( + + + + Edition name + { + setName(e.target.value); + setNameError(undefined); + setError(undefined); + }} + /> + {nameError} + + + + Edition year + { + setYear(e.target.value); + setYearError(undefined); + setError(undefined); + }} + /> + {yearError} + + {submitButton} + {error} + + + ); } diff --git a/frontend/src/views/CreateEditionPage/styles.ts b/frontend/src/views/CreateEditionPage/styles.ts new file mode 100644 index 000000000..4cfef2415 --- /dev/null +++ b/frontend/src/views/CreateEditionPage/styles.ts @@ -0,0 +1,24 @@ +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; +`; + +export const CreateEditionDiv = styled.div` + width: 80%; + max-width: 500px; + margin: auto; +`; + +export const FormGroup = styled(Form.Group)` + margin-top: 20px; +`; + +export const ButtonDiv = styled.div` + margin-top: 20px; + margin-bottom: 20px; + float: right; +`; diff --git a/frontend/src/views/index.ts b/frontend/src/views/index.ts index c378482b5..a72d402c5 100644 --- a/frontend/src/views/index.ts +++ b/frontend/src/views/index.ts @@ -1,6 +1,7 @@ export * as Errors from "./errors"; 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 { default as ProjectsPage } from "./ProjectsPage"; export { default as RegisterPage } from "./RegisterPage"; From f9f6cc1f0d6280fc7d455ad760cb3f78d586e7cd Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sat, 23 Apr 2022 21:47:34 +0200 Subject: [PATCH 463/536] pagination is functional --- .../ProjectsPage/ProjectsPage.tsx | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index 24504295c..2df6c2dd5 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -14,6 +14,7 @@ import { Button } from "react-bootstrap"; export default function ProjectPage() { const [projectsAPI, setProjectsAPI] = useState([]); const [gotProjects, setGotProjects] = useState(false); + const [moreProjectsAvailable, setMoreProjectsAvailable] = useState(true); // Endpoint has more coaches available // To filter projects we need to keep a separate list to avoid calling the API every time we change te filters. const [projects, setProjects] = useState([]); @@ -22,7 +23,7 @@ export default function ProjectPage() { const [searchString, setSearchString] = useState(""); const [ownProjects, setOwnProjects] = useState(false); - const [page, setPage] = useState(0) + const [page, setPage] = useState(0); const { userId } = useAuth(); @@ -60,13 +61,17 @@ export default function ProjectPage() { setGotProjects(true); const response = await getProjects(editionId, page); if (response) { - setProjectsAPI(response.projects); - setProjects(response.projects); + if (response.projects.length === 0) { + setMoreProjectsAvailable(false); + } + setProjectsAPI(projectsAPI.concat(response.projects)); + setProjects(projects.concat(response.projects)); } } - callProjects(); - - }, [editionId, gotProjects, page]); + if (moreProjectsAvailable && !gotProjects) { + callProjects(); + } + }, [editionId, gotProjects, moreProjectsAvailable, page, projects, projectsAPI]); return (
                                    @@ -93,13 +98,26 @@ export default function ProjectPage() { {projects.map((project, _index) => ( setGotProjects(false)} + refreshProjects={() => { + setProjectsAPI([]); + setProjects([]); + setGotProjects(false); + setPage(0); + setMoreProjectsAvailable(true); + }} key={_index} /> ))} - +
                                    ); } From 0ef625652996926624f916e44a801d003549fd71 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Sat, 23 Apr 2022 22:06:05 +0200 Subject: [PATCH 464/536] added filter --- backend/src/app/logic/projects.py | 8 +-- .../app/routers/editions/projects/projects.py | 13 +++-- backend/src/app/schemas/projects.py | 9 +++ backend/src/database/crud/projects.py | 41 ++++++++++---- .../test_database/test_crud/test_projects.py | 56 ++++++++++++++----- .../test_projects/test_projects.py | 27 ++++++++- 6 files changed, 117 insertions(+), 37 deletions(-) diff --git a/backend/src/app/logic/projects.py b/backend/src/app/logic/projects.py index 3d2588167..a733f143f 100644 --- a/backend/src/app/logic/projects.py +++ b/backend/src/app/logic/projects.py @@ -2,14 +2,14 @@ import src.database.crud.projects as crud from src.app.schemas.projects import ( - ProjectList, ConflictStudentList, InputProject, ConflictStudent + ProjectList, ConflictStudentList, InputProject, ConflictStudent, QueryParamsProjects ) -from src.database.models import Edition, Project +from src.database.models import Edition, Project, User -def get_project_list(db: Session, edition: Edition, page: int) -> ProjectList: +def get_project_list(db: Session, 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, page)) + return ProjectList(projects=crud.get_projects_for_edition_page(db, edition, search_params, user)) def create_project(db: Session, edition: Edition, input_project: InputProject) -> Project: diff --git a/backend/src/app/routers/editions/projects/projects.py b/backend/src/app/routers/editions/projects/projects.py index 3b58523dd..0c55a326c 100644 --- a/backend/src/app/routers/editions/projects/projects.py +++ b/backend/src/app/routers/editions/projects/projects.py @@ -6,21 +6,24 @@ 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 +from src.app.schemas.projects import (ProjectList, Project, InputProject, + ConflictStudentList, QueryParamsProjects) from src.database.database import get_session -from src.database.models import Edition, Project as ProjectModel +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.get("/", response_model=ProjectList, dependencies=[Depends(require_coach)]) -async def get_projects(db: Session = Depends(get_session), edition: Edition = Depends(get_edition), page: int = 0): +@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, page) + return logic.get_project_list(db, edition, search_params, user) @projects_router.post("/", status_code=status.HTTP_201_CREATED, response_model=Project, diff --git a/backend/src/app/schemas/projects.py b/backend/src/app/schemas/projects.py index 3fe1d0991..93664116d 100644 --- a/backend/src/app/schemas/projects.py +++ b/backend/src/app/schemas/projects.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from pydantic import BaseModel from src.app.schemas.utils import CamelCaseModel @@ -117,3 +118,11 @@ class InputProject(BaseModel): class InputStudentRole(BaseModel): """Used for creating/patching a student role""" skill_id: int + + +@dataclass +class QueryParamsProjects: + """search query parameters for projects""" + name: str = "" + coach: bool = False + page: int = 0 diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index 390579f62..de2b6e9f2 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -1,7 +1,8 @@ +from sqlalchemy import intersect from sqlalchemy.exc import NoResultFound from sqlalchemy.orm import Session, Query -from src.app.schemas.projects import InputProject +from src.app.schemas.projects import InputProject, QueryParamsProjects from src.database.crud.util import paginate from src.database.models import Project, Edition, Student, ProjectRole, Skill, User, Partner @@ -15,9 +16,16 @@ def get_projects_for_edition(db: Session, edition: Edition) -> list[Project]: return _get_projects_for_edition_query(db, edition).all() -def get_projects_for_edition_page(db: Session, edition: Edition, page: int) -> list[Project]: +def get_projects_for_edition_page(db: Session, edition: Edition, + search_params: QueryParamsProjects, user: User) -> list[Project]: """Returns a paginated list of all projects from a certain edition from the database""" - return paginate(_get_projects_for_edition_query(db, edition), page).all() + query = _get_projects_for_edition_query(db, edition).where( + Project.name.contains(search_params.name)) + projects: list[Project] = paginate(query, search_params.page).all() + if not search_params.coach: + return projects + return list(set(projects) & set(user.projects)) + def add_project(db: Session, edition: Edition, input_project: InputProject) -> Project: @@ -25,12 +33,15 @@ def add_project(db: Session, edition: Edition, input_project: InputProject) -> P 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] + 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()) + partners_obj.append(db.query(Partner).where( + Partner.name == partner).one()) except NoResultFound: partner_obj = Partner(name=partner) db.add(partner_obj) @@ -50,7 +61,8 @@ def get_project(db: Session, project_id: int) -> Project: def delete_project(db: Session, project_id: int): """Delete a specific project from the database""" - proj_roles = db.query(ProjectRole).where(ProjectRole.project_id == project_id).all() + proj_roles = db.query(ProjectRole).where( + ProjectRole.project_id == project_id).all() for proj_role in proj_roles: db.delete(proj_role) @@ -66,12 +78,15 @@ def patch_project(db: Session, project_id: int, input_project: InputProject): """ 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] + 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()) + partners_obj.append(db.query(Partner).where( + Partner.name == partner).one()) except NoResultFound: partner_obj = Partner(name=partner) db.add(partner_obj) @@ -96,10 +111,12 @@ def get_conflict_students(db: Session, edition: Edition) -> list[tuple[Student, 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() + 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() + proj = db.query(Project).where( + Project.project_id == proj_id).one() projs.append(proj) conflict_student = (student, projs) conflict_students.append(conflict_student) diff --git a/backend/tests/test_database/test_crud/test_projects.py b/backend/tests/test_database/test_crud/test_projects.py index d7e2b2b99..5e3ca22d2 100644 --- a/backend/tests/test_database/test_crud/test_projects.py +++ b/backend/tests/test_database/test_crud/test_projects.py @@ -1,9 +1,10 @@ import pytest +from sqlalchemy import null from sqlalchemy.exc import NoResultFound from sqlalchemy.orm import Session from settings import DB_PAGE_SIZE -from src.app.schemas.projects import InputProject +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 @@ -13,14 +14,15 @@ 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="project3", 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) - 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") @@ -65,7 +67,8 @@ def test_get_all_projects_empty(database_session: Session): def test_get_all_projects(database_with_data: Session, current_edition: Edition): """test get all projects""" - projects: list[Project] = crud.get_projects_for_edition(database_with_data, current_edition) + projects: list[Project] = crud.get_projects_for_edition( + database_with_data, current_edition) assert len(projects) == 3 @@ -75,15 +78,35 @@ def test_get_all_projects_pagination(database_session: Session): 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.add( + Project(name=f"Project {i}", edition=edition, number_of_students=5)) database_session.commit() - assert len(crud.get_projects_for_edition_page(database_session, edition, 0)) == DB_PAGE_SIZE - assert len(crud.get_projects_for_edition_page(database_session, edition, 1)) == round( + 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 +def test_get_project_search_name(database_with_data: Session): + """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) + assert len(projects) == 1 + assert projects[0].name == "super nice project" + + +def test_get_project_search_coach(database_with_data: Session): + """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) + assert len(projects) == 1 + + def test_add_project_partner_do_not_exist_yet(database_with_data: Session, current_edition: Edition): """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], @@ -150,9 +173,11 @@ def test_delete_project_no_project_roles(database_with_data: Session, current_ed """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 + 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 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)] @@ -161,9 +186,11 @@ def test_delete_project_with_project_roles(database_with_data: Session, current_ """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 + 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 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( @@ -186,7 +213,7 @@ def test_patch_project(database_with_data: Session, current_edition: Edition): 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) + proj_patched) assert new_partner in new_project.partners assert new_project.name == "project1" @@ -194,7 +221,8 @@ def test_patch_project(database_with_data: Session, current_edition: Edition): def test_get_conflict_students(database_with_data: Session, current_edition: Edition): """test if the right ConflictStudent is given""" - conflicts: list[(Student, list[Project])] = crud.get_conflict_students(database_with_data, current_edition) + conflicts: list[(Student, list[Project])] = crud.get_conflict_students( + database_with_data, current_edition) assert len(conflicts) == 1 assert conflicts[0][0].student_id == 1 assert len(conflicts[0][1]) == 2 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 cad926d79..556604c90 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 @@ -14,7 +14,7 @@ def database_with_data(database_session: Session) -> Session: 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) + project3 = Project(name="super nice project", edition=edition, number_of_students=3) database_session.add(project1) database_session.add(project2) database_session.add(project3) @@ -61,7 +61,7 @@ def test_get_projects(database_with_data: Session, auth_client: AuthClient): assert len(json['projects']) == 3 assert json['projects'][0]['name'] == "project1" assert json['projects'][1]['name'] == "project2" - assert json['projects'][2]['name'] == "project3" + assert json['projects'][2]['name'] == "super nice project" def test_get_projects_paginated(database_session: Session, auth_client: AuthClient): @@ -308,3 +308,26 @@ def test_create_project_old_edition(database_with_data: Session, auth_client: Au "skills": [1, 1, 1, 1, 1], "partners": ["ugent"], "coaches": [1]}) assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + + +def test_search_project_name(database_with_data: Session, 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" + + +def test_search_project_coach(database_with_data: Session, 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 From cb38e47183f69f561d4509c8006be7b02e9feed2 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Sat, 23 Apr 2022 22:08:02 +0200 Subject: [PATCH 465/536] delete unused import --- backend/src/database/crud/projects.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index de2b6e9f2..bd97f61f3 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -1,4 +1,3 @@ -from sqlalchemy import intersect from sqlalchemy.exc import NoResultFound from sqlalchemy.orm import Session, Query From 551e7d5e41fd338b2be28e575c3859158480e45d Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Sat, 23 Apr 2022 22:11:07 +0200 Subject: [PATCH 466/536] deleted an unused import again --- backend/tests/test_database/test_crud/test_projects.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/tests/test_database/test_crud/test_projects.py b/backend/tests/test_database/test_crud/test_projects.py index 5e3ca22d2..c9cd146e8 100644 --- a/backend/tests/test_database/test_crud/test_projects.py +++ b/backend/tests/test_database/test_crud/test_projects.py @@ -1,5 +1,4 @@ import pytest -from sqlalchemy import null from sqlalchemy.exc import NoResultFound from sqlalchemy.orm import Session From 7c6ef961587951c9bf92b544bf702576a859d1f0 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Sun, 24 Apr 2022 14:46:40 +0200 Subject: [PATCH 467/536] unwrap UserData --- backend/src/app/routers/login/login.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/app/routers/login/login.py b/backend/src/app/routers/login/login.py index 303400937..ed3d1434d 100644 --- a/backend/src/app/routers/login/login.py +++ b/backend/src/app/routers/login/login.py @@ -37,7 +37,7 @@ async def login_for_access_token(db: Session = Depends(get_session), access_token=access_token, refresh_token=refresh_token, token_type="bearer", - user=UserData(**user_data) + user=user_data ) @@ -55,5 +55,5 @@ async def refresh_access_token(db: Session = Depends(get_session), user: User = access_token=access_token, refresh_token=refresh_token, token_type="bearer", - user=UserData(**user_data) + user=user_data ) From a2612e96d475cd00b6a9543eb8aa9674b6237e7d Mon Sep 17 00:00:00 2001 From: FKD13 Date: Sun, 24 Apr 2022 14:50:45 +0200 Subject: [PATCH 468/536] extract to function --- backend/src/app/routers/login/login.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/backend/src/app/routers/login/login.py b/backend/src/app/routers/login/login.py index ed3d1434d..117ec7221 100644 --- a/backend/src/app/routers/login/login.py +++ b/backend/src/app/routers/login/login.py @@ -8,7 +8,7 @@ from src.app.logic.security import authenticate_user, create_tokens from src.app.logic.users import get_user_editions from src.app.routers.tags import Tags -from src.app.schemas.login import Token, UserData +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.database.database import get_session @@ -28,24 +28,20 @@ async def login_for_access_token(db: Session = Depends(get_session), # be a 401 instead of a 404 raise InvalidCredentialsException() from not_found - access_token, refresh_token = create_tokens(user) - - user_data: dict = user_model_to_schema(user).__dict__ - user_data["editions"] = get_user_editions(db, user) - - return Token( - access_token=access_token, - refresh_token=refresh_token, - token_type="bearer", - user=user_data - ) + 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)): - """Return a new access & refresh token using on the old refresh token + """ + Return a new access & refresh token using on the old refresh token + + Swagger note: This endpoint will not work on swagger because it uses the access token to try & refresh + """ + return await generate_token_response_for_user(db, user) + - Swagger note: This endpoint will not work on swagger because it uses the access token to try & refresh""" +async def generate_token_response_for_user(db: Session, user: User) -> Token: access_token, refresh_token = create_tokens(user) user_data: dict = user_model_to_schema(user).__dict__ From c5398c33bce81dfc1171e9fa6e32ed3c8d5ac7e6 Mon Sep 17 00:00:00 2001 From: Francis <44001949+FKD13@users.noreply.github.com> Date: Sun, 24 Apr 2022 14:51:19 +0200 Subject: [PATCH 469/536] Update backend/src/app/exceptions/handlers.py Co-authored-by: Stijn De Clercq <60451863+stijndcl@users.noreply.github.com> --- backend/src/app/exceptions/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/exceptions/handlers.py b/backend/src/app/exceptions/handlers.py index afa50b5ab..0a4d96e2b 100644 --- a/backend/src/app/exceptions/handlers.py +++ b/backend/src/app/exceptions/handlers.py @@ -101,7 +101,7 @@ def failed_to_add_project_role_exception(_request: Request, _exception: FailedTo async def wrong_token_type_exception(_request: Request, _exception: WrongTokenTypeException): return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, - content={'message': 'U used the wrong token to access this resource.'} + content={'message': 'You used the wrong token to access this resource.'} ) @app.exception_handler(ReadOnlyEditionException) def read_only_edition_exception(_request: Request, _exception: ReadOnlyEditionException): From ee5ed470ac0e9ebd66c327565392a68372b1ef84 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Sun, 24 Apr 2022 14:54:10 +0200 Subject: [PATCH 470/536] rename dependency --- backend/src/app/routers/login/login.py | 3 +-- backend/src/app/routers/users/users.py | 4 ++-- backend/src/app/utils/dependencies.py | 14 ++++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/backend/src/app/routers/login/login.py b/backend/src/app/routers/login/login.py index 117ec7221..b833abc57 100644 --- a/backend/src/app/routers/login/login.py +++ b/backend/src/app/routers/login/login.py @@ -18,8 +18,7 @@ @login_router.post("/token", response_model=Token) -async def login_for_access_token(db: Session = Depends(get_session), - form_data: OAuth2PasswordRequestForm = Depends()): +async def login_for_access_token(db: Session = 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) diff --git a/backend/src/app/routers/users/users.py b/backend/src/app/routers/users/users.py index b82f1a98f..75f1398c8 100644 --- a/backend/src/app/routers/users/users.py +++ b/backend/src/app/routers/users/users.py @@ -6,7 +6,7 @@ from src.app.schemas.login import UserData from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse, user_model_to_schema, \ FilterParameters -from src.app.utils.dependencies import require_admin, get_current_active_user +from src.app.utils.dependencies import require_admin, get_user_from_access_token from src.database.database import get_session from src.database.models import User as UserDB @@ -27,7 +27,7 @@ async def get_users( @users_router.get("/current", response_model=UserData) -async def get_current_user(db: Session = Depends(get_session), user: UserDB = Depends(get_current_active_user)): +async def get_current_user(db: Session = 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) diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index 9aa0bd243..28998751d 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -13,21 +13,23 @@ from src.app.logic.security import ALGORITHM, TokenType from src.database.crud.editions import get_edition_by_name, latest_edition 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.crud.students import get_student_by_id -from src.database.crud.suggestions import get_suggestion_by_id def get_edition(edition_name: str, database: Session = Depends(get_session)) -> Edition: """Get an edition from the database, given the name in the path""" return get_edition_by_name(database, edition_name) + def get_student(student_id: int, database: Session = Depends(get_session)) -> Student: """Get the student from the database, given the id in the path""" return get_student_by_id(database, student_id) + def get_suggestion(suggestion_id: int, database: Session = Depends(get_session)) -> Suggestion: """Get the suggestion from the database, given the id in the path""" return get_suggestion_by_id(database, suggestion_id) @@ -69,7 +71,7 @@ async def _get_user_from_token(token_type: TokenType, db: Session, token: str) - raise InvalidCredentialsException() from jwt_err -async def get_current_active_user(db: Session = Depends(get_session), token: str = Depends(oauth2_scheme)) -> User: +async def get_user_from_access_token(db: Session = 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 """ @@ -83,7 +85,7 @@ async def get_user_from_refresh_token(db: Session = Depends(get_session), token: return await _get_user_from_token(TokenType.REFRESH, db, token) -async def require_auth(user: User = Depends(get_current_active_user)) -> User: +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 editions @@ -102,7 +104,7 @@ async def require_auth(user: User = Depends(get_current_active_user)) -> User: return user -async def require_admin(user: User = Depends(get_current_active_user)) -> User: +async def require_admin(user: User = Depends(get_user_from_access_token)) -> User: """Dependency to create an admin-only route""" if not user.admin: raise MissingPermissionsException() @@ -110,7 +112,7 @@ async def require_admin(user: User = Depends(get_current_active_user)) -> User: return user -async def require_coach(edition: Edition = Depends(get_edition), user: User = Depends(get_current_active_user)) -> User: +async def require_coach(edition: Edition = Depends(get_edition), user: User = Depends(get_user_from_access_token)) -> User: """Dependency to check if a user can see a given resource This comes down to checking if a coach is linked to an edition or not """ From d49d1a47095604e55e0067f7197cf9dfbc3cb894 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Sun, 24 Apr 2022 14:55:03 +0200 Subject: [PATCH 471/536] wait 100ms --- frontend/src/utils/api/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/utils/api/api.ts b/frontend/src/utils/api/api.ts index abdd0ab36..8c48e6af0 100644 --- a/frontend/src/utils/api/api.ts +++ b/frontend/src/utils/api/api.ts @@ -16,7 +16,7 @@ axiosInstance.defaults.baseURL = BASE_URL; axiosInstance.interceptors.request.use(async config => { // If the request is sent when a token is being refreshed, delay it for 100ms. while (getRefreshTokenLock()) { - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise(resolve => setTimeout(resolve, 100)); } const accessToken = getAccessToken(); if (accessToken) { From 285fafadcfc2ee4b3a43c4ef84bc11f75daca2e0 Mon Sep 17 00:00:00 2001 From: FKD13 Date: Sun, 24 Apr 2022 14:58:18 +0200 Subject: [PATCH 472/536] add comments --- frontend/src/utils/api/auth.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/api/auth.ts b/frontend/src/utils/api/auth.ts index 6ab4f50ad..3ba6934dc 100644 --- a/frontend/src/utils/api/auth.ts +++ b/frontend/src/utils/api/auth.ts @@ -47,13 +47,17 @@ export async function validateRegistrationUrl(edition: string, uuid: string): Pr } } +/** + * Interface containg the newly fetched tokens. + */ export interface Tokens { access_token: string; refresh_token: string; } /** - * + * Function to fetch the new tokens based on the refreshtoken. + * We use a separate axios intance here because this request would otherwise be blocked by our interceptor. */ export async function refreshTokens(): Promise { // Don't use axiosInstance to pass interceptors. From b59dd96f9ccf57dc5e89be2049dd217cdcb2457c Mon Sep 17 00:00:00 2001 From: FKD13 Date: Sun, 24 Apr 2022 15:01:04 +0200 Subject: [PATCH 473/536] fix linting --- backend/src/app/routers/login/login.py | 3 +++ backend/src/app/utils/dependencies.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/app/routers/login/login.py b/backend/src/app/routers/login/login.py index b833abc57..093f5cd13 100644 --- a/backend/src/app/routers/login/login.py +++ b/backend/src/app/routers/login/login.py @@ -41,6 +41,9 @@ async def refresh_access_token(db: Session = Depends(get_session), user: User = async def generate_token_response_for_user(db: Session, 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__ diff --git a/backend/src/app/utils/dependencies.py b/backend/src/app/utils/dependencies.py index 28998751d..1e6043417 100644 --- a/backend/src/app/utils/dependencies.py +++ b/backend/src/app/utils/dependencies.py @@ -112,7 +112,8 @@ async def require_admin(user: User = Depends(get_user_from_access_token)) -> Use return user -async def require_coach(edition: Edition = Depends(get_edition), user: User = Depends(get_user_from_access_token)) -> User: +async def require_coach(edition: Edition = Depends(get_edition), + user: User = Depends(get_user_from_access_token)) -> User: """Dependency to check if a user can see a given resource This comes down to checking if a coach is linked to an edition or not """ From ec2df307fcf0f6e7889d0210ba587ecf399deedd Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 24 Apr 2022 16:04:30 +0200 Subject: [PATCH 474/536] pagination works with a load spinner but infinite scroll is still not working --- frontend/package.json | 2 + .../LoadSpinner/LoadSpinner.tsx | 11 ++ .../ProjectsComponents/LoadSpinner/index.ts | 1 + .../ProjectsComponents/LoadSpinner/styles.ts | 7 ++ .../components/ProjectsComponents/index.ts | 1 + frontend/src/utils/api/projects.ts | 2 +- .../ProjectsPage/ProjectsPage.tsx | 110 +++++++++++------- .../views/projectViews/ProjectsPage/styles.ts | 20 +++- frontend/yarn.lock | 16 ++- 9 files changed, 128 insertions(+), 42 deletions(-) create mode 100644 frontend/src/components/ProjectsComponents/LoadSpinner/LoadSpinner.tsx create mode 100644 frontend/src/components/ProjectsComponents/LoadSpinner/index.ts create mode 100644 frontend/src/components/ProjectsComponents/LoadSpinner/styles.ts diff --git a/frontend/package.json b/frontend/package.json index 5b835baaf..708208b16 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,6 +6,7 @@ "@fortawesome/fontawesome-svg-core": "^1.3.0", "@fortawesome/free-solid-svg-icons": "^6.0.0", "@fortawesome/react-fontawesome": "^0.1.17", + "@types/react-infinite-scroller": "^1.2.3", "axios": "^0.26.1", "bootstrap": "5.1.3", "buffer": "^6.0.3", @@ -13,6 +14,7 @@ "react-bootstrap": "^2.2.1", "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", diff --git a/frontend/src/components/ProjectsComponents/LoadSpinner/LoadSpinner.tsx b/frontend/src/components/ProjectsComponents/LoadSpinner/LoadSpinner.tsx new file mode 100644 index 000000000..d46544ab0 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/LoadSpinner/LoadSpinner.tsx @@ -0,0 +1,11 @@ +import { Spinner } from "react-bootstrap"; +import { SpinnerContainer } from "./styles"; + +export default function LoadSpinner({ show }: { show: boolean }) { + if (!show) return null; + return ( + + + + ); +} diff --git a/frontend/src/components/ProjectsComponents/LoadSpinner/index.ts b/frontend/src/components/ProjectsComponents/LoadSpinner/index.ts new file mode 100644 index 000000000..4e1c2fe09 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/LoadSpinner/index.ts @@ -0,0 +1 @@ +export { default } from "./LoadSpinner"; diff --git a/frontend/src/components/ProjectsComponents/LoadSpinner/styles.ts b/frontend/src/components/ProjectsComponents/LoadSpinner/styles.ts new file mode 100644 index 000000000..1ae848123 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/LoadSpinner/styles.ts @@ -0,0 +1,7 @@ +import styled from "styled-components"; + +export const SpinnerContainer = styled.div` + display: flex; + justify-content: center; + margin: 20px; +`; diff --git a/frontend/src/components/ProjectsComponents/index.ts b/frontend/src/components/ProjectsComponents/index.ts index 821e4083e..a6c527b3a 100644 --- a/frontend/src/components/ProjectsComponents/index.ts +++ b/frontend/src/components/ProjectsComponents/index.ts @@ -1,2 +1,3 @@ export { default as ProjectCard } from "./ProjectCard"; export { default as StudentPlaceholder } from "./StudentPlaceholder"; +export { default as LoadSpinner } from "./LoadSpinner"; diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index 8c806a330..a5fbe3aa8 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -4,7 +4,7 @@ import { axiosInstance } from "./api"; export async function getProjects(edition: string, page: number) { try { - const response = await axiosInstance.get("/editions/" + edition + "/projects?page=" + page.toString()); + const response = await axiosInstance.get("/editions/" + edition + "/projects/?page=" + page.toString()); const projects = response.data as Projects; return projects; } catch (error) { diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index 2df6c2dd5..634758ea5 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -1,12 +1,20 @@ import { useEffect, useState } from "react"; import { getProjects } from "../../../utils/api/projects"; -import { ProjectCard } from "../../../components/ProjectsComponents"; -import { CardsGrid, CreateButton, SearchButton, SearchField, OwnProject } from "./styles"; +import { ProjectCard, LoadSpinner } from "../../../components/ProjectsComponents"; +import { + CardsGrid, + CreateButton, + SearchButton, + SearchField, + OwnProject, + ProjectsContainer, + LoadMoreContainer, + LoadMoreButton, +} from "./styles"; import { useAuth } from "../../../contexts/auth-context"; import { Project } from "../../../data/interfaces"; import { useParams } from "react-router-dom"; -import { Button } from "react-bootstrap"; - +import InfiniteScroll from "react-infinite-scroller"; /** * @returns The projects overview page where you can see all the projects. * You can filter on your own projects or filter on project name. @@ -14,6 +22,7 @@ import { Button } from "react-bootstrap"; export default function ProjectPage() { const [projectsAPI, setProjectsAPI] = useState([]); const [gotProjects, setGotProjects] = useState(false); + const [loading, setLoading] = useState(false); const [moreProjectsAvailable, setMoreProjectsAvailable] = useState(true); // Endpoint has more coaches available // To filter projects we need to keep a separate list to avoid calling the API every time we change te filters. @@ -56,22 +65,28 @@ export default function ProjectPage() { /** * Used to fetch the projects */ - useEffect(() => { - async function callProjects() { - setGotProjects(true); - const response = await getProjects(editionId, page); - if (response) { - if (response.projects.length === 0) { - setMoreProjectsAvailable(false); - } - setProjectsAPI(projectsAPI.concat(response.projects)); - setProjects(projects.concat(response.projects)); - } + async function callProjects(newPage: number) { + if (loading) return; + setLoading(true); + const response = await getProjects(editionId, newPage); + setGotProjects(true); + + if (response) { + if (response.projects.length === 0) { + setMoreProjectsAvailable(false); + } else setPage(page + 1); + + setProjectsAPI(projectsAPI.concat(response.projects)); + setProjects(projects.concat(response.projects)); } + setLoading(false); + } + + useEffect(() => { if (moreProjectsAvailable && !gotProjects) { - callProjects(); + callProjects(0); } - }, [editionId, gotProjects, moreProjectsAvailable, page, projects, projectsAPI]); + }); return (
                                    @@ -94,30 +109,47 @@ export default function ProjectPage() { }} /> - - {projects.map((project, _index) => ( - { - setProjectsAPI([]); - setProjects([]); - setGotProjects(false); - setPage(0); - setMoreProjectsAvailable(true); - }} - key={_index} - /> - ))} - - - + + + {projects.map((project, _index) => ( + { + setProjectsAPI([]); + setProjects([]); + setGotProjects(false); + setPage(0); + setMoreProjectsAvailable(true); + }} + key={_index} + /> + ))} + + + + + + + + { + 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 787324608..ddbe9537b 100644 --- a/frontend/src/views/projectViews/ProjectsPage/styles.ts +++ b/frontend/src/views/projectViews/ProjectsPage/styles.ts @@ -4,7 +4,7 @@ import { Form } from "react-bootstrap"; export const CardsGrid = styled.div` display: grid; grid-gap: 5px; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(600px, 1fr)); grid-auto-flow: dense; `; @@ -37,3 +37,21 @@ export const OwnProject = styled(Form.Check)` margin-top: 10px; margin-left: 20px; `; + +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 db25e1b10..014e849cd 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1989,6 +1989,13 @@ dependencies: "@types/react" "*" +"@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" + integrity sha512-l60JckVoO+dxmKW2eEG7jbliEpITsTJvRPTe97GazjF5+ylagAuyYdXl8YY9DQsTP9QjhqGKZROknzgscGJy0A== + dependencies: + "@types/react" "*" + "@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" @@ -7512,7 +7519,7 @@ prop-types-extra@^1.1.0: react-is "^16.3.2" warning "^4.0.0" -prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.5.8, 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" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -7684,6 +7691,13 @@ react-icons@^4.3.1: resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.3.1.tgz#2fa92aebbbc71f43d2db2ed1aed07361124e91ca" 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" + 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" From 8331e21513b69756b6028f6a102257e8129d043b Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 24 Apr 2022 16:15:48 +0200 Subject: [PATCH 475/536] Common load spinner everybody can use --- .../components/CommonComps/LoadSpinner/LoadSpinner.tsx | 10 ++++++++++ .../src/components/CommonComps/LoadSpinner/index.ts | 1 + .../src/components/CommonComps/LoadSpinner/styles.ts | 7 +++++++ frontend/src/components/CommonComps/index.ts | 1 + 4 files changed, 19 insertions(+) create mode 100644 frontend/src/components/CommonComps/LoadSpinner/LoadSpinner.tsx create mode 100644 frontend/src/components/CommonComps/LoadSpinner/index.ts create mode 100644 frontend/src/components/CommonComps/LoadSpinner/styles.ts create mode 100644 frontend/src/components/CommonComps/index.ts diff --git a/frontend/src/components/CommonComps/LoadSpinner/LoadSpinner.tsx b/frontend/src/components/CommonComps/LoadSpinner/LoadSpinner.tsx new file mode 100644 index 000000000..b773008ec --- /dev/null +++ b/frontend/src/components/CommonComps/LoadSpinner/LoadSpinner.tsx @@ -0,0 +1,10 @@ +import { Spinner } from "react-bootstrap"; +import { SpinnerContainer } from "./styles"; +export default function LoadSpinner({ show }: { show: boolean }) { + if (!show) return null; + return ( + + + + ); +} diff --git a/frontend/src/components/CommonComps/LoadSpinner/index.ts b/frontend/src/components/CommonComps/LoadSpinner/index.ts new file mode 100644 index 000000000..4e1c2fe09 --- /dev/null +++ b/frontend/src/components/CommonComps/LoadSpinner/index.ts @@ -0,0 +1 @@ +export { default } from "./LoadSpinner"; diff --git a/frontend/src/components/CommonComps/LoadSpinner/styles.ts b/frontend/src/components/CommonComps/LoadSpinner/styles.ts new file mode 100644 index 000000000..1ae848123 --- /dev/null +++ b/frontend/src/components/CommonComps/LoadSpinner/styles.ts @@ -0,0 +1,7 @@ +import styled from "styled-components"; + +export const SpinnerContainer = styled.div` + display: flex; + justify-content: center; + margin: 20px; +`; diff --git a/frontend/src/components/CommonComps/index.ts b/frontend/src/components/CommonComps/index.ts new file mode 100644 index 000000000..507a08be1 --- /dev/null +++ b/frontend/src/components/CommonComps/index.ts @@ -0,0 +1 @@ +export { default as LoadSpinner } from "./LoadSpinner"; From 96f42880f14d2d631d3c40371482c11e1387cd6a Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 24 Apr 2022 16:22:30 +0200 Subject: [PATCH 476/536] add documentation --- .../src/components/CommonComps/LoadSpinner/LoadSpinner.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/components/CommonComps/LoadSpinner/LoadSpinner.tsx b/frontend/src/components/CommonComps/LoadSpinner/LoadSpinner.tsx index b773008ec..3d2fec943 100644 --- a/frontend/src/components/CommonComps/LoadSpinner/LoadSpinner.tsx +++ b/frontend/src/components/CommonComps/LoadSpinner/LoadSpinner.tsx @@ -1,5 +1,10 @@ import { Spinner } from "react-bootstrap"; import { SpinnerContainer } from "./styles"; +/** + * + * @param show: whether to show the spinner or not + * @returns a spinner to display when data is being fetched + */ export default function LoadSpinner({ show }: { show: boolean }) { if (!show) return null; return ( From d50c49cc239f93cb25aeea4bae93cf2d11699d6e Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 24 Apr 2022 16:38:08 +0200 Subject: [PATCH 477/536] small fix when there are no more projects --- .../src/views/projectViews/ProjectsPage/ProjectsPage.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index 634758ea5..41e6f0613 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -74,10 +74,11 @@ export default function ProjectPage() { if (response) { if (response.projects.length === 0) { setMoreProjectsAvailable(false); - } else setPage(page + 1); - - setProjectsAPI(projectsAPI.concat(response.projects)); - setProjects(projects.concat(response.projects)); + } else { + setPage(page + 1); + setProjectsAPI(projectsAPI.concat(response.projects)); + setProjects(projects.concat(response.projects)); + } } setLoading(false); } From 82ed93bb05bb6ed303bbdb22465f0b887d623250 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Sun, 24 Apr 2022 17:55:13 +0200 Subject: [PATCH 478/536] placeholders for adding skills --- .../AddedSkills/AddedSkills.tsx | 58 +++++++++++++++++++ .../InputFields/Coach/Coach.tsx | 10 ++-- .../InputFields/Skill/Skill.tsx | 44 ++++++++++++-- frontend/src/data/interfaces/projects.ts | 14 +++++ .../CreateProjectPage/CreateProjectPage.tsx | 13 ++++- 5 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills.tsx diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills.tsx new file mode 100644 index 000000000..06fa1ef40 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills.tsx @@ -0,0 +1,58 @@ +import { SkillProject } from "../../../../data/interfaces/projects"; + +export default function AddedSkills({ + skills, + setSkills, +}: { + skills: SkillProject[]; + setSkills: (skills: SkillProject[]) => void; +}) { + return ( +
                                    + {skills.map((skill, _index) => ( +
                                    + {skill.skill} + { + const newList = skills.map((item, otherIndex) => { + if (_index === otherIndex) { + const updatedItem = { + ...item, + amount: event.target.valueAsNumber, + }; + return updatedItem; + } + return item; + }); + + setSkills(newList); + }} + /> + { + const newList = skills.map((item, otherIndex) => { + if (_index === otherIndex) { + const updatedItem = { + ...item, + description: event.target.value, + }; + return updatedItem; + } + return item; + }); + + setSkills(newList); + }} + /> +
                                    + ))} +
                                    + ); +} diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx index 65dfaa2ad..544330504 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx @@ -33,10 +33,12 @@ export default function Coach({ { if (availableCoaches.some(availableCoach => availableCoach === coach)) { - const newCoaches = [...coaches]; - newCoaches.push(coach); - setCoaches(newCoaches); - setShowAlert(false); + if (!coaches.includes(coach)) { + const newCoaches = [...coaches]; + newCoaches.push(coach); + setCoaches(newCoaches); + setShowAlert(false); + } } else setShowAlert(true); }} > diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx index fc67d4654..1746ab745 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx @@ -1,16 +1,52 @@ +import { SkillProject } from "../../../../../data/interfaces/projects"; import { Input, AddButton } from "../../styles"; export default function Skill({ + skill, + setSkill, skills, setSkills, }: { - skills: string[]; - setSkills: (skills: string[]) => void; + skill: string; + setSkill: (skill: string) => void; + skills: SkillProject[]; + setSkills: (skills: SkillProject[]) => void; }) { + const availableSkills = ["Frontend", "Backend", "Database", "Design"]; + return (
                                    - setSkills([])} placeholder="Skill" /> - Add skill + setSkill(e.target.value)} + placeholder="Skill" + list="skills" + /> + + {availableSkills.map((availableCoach, _index) => { + return + + { + + if (availableSkills.some(availableSkill => availableSkill === skill)) { + if (!skills.some(existingSkill => existingSkill.skill === skill)) { + const newSkills = [...skills]; + const newSkill: SkillProject = { + skill: skill, + description: undefined, + amount: undefined, + }; + newSkills.push(newSkill); + setSkills(newSkills); + } + } + }} + > + Add skill +
                                    ); } diff --git a/frontend/src/data/interfaces/projects.ts b/frontend/src/data/interfaces/projects.ts index 43051f1dd..18aeb1730 100644 --- a/frontend/src/data/interfaces/projects.ts +++ b/frontend/src/data/interfaces/projects.ts @@ -53,6 +53,20 @@ export interface Projects { projects: Project[]; } +/** + * Used to add skills to a project + */ +export interface SkillProject { + /** The name of the skill */ + skill: string; + + /** More info about this skill in a specific project */ + description: string | undefined; + + /** Number of positions of this skill in a project */ + amount: number | undefined; +} + /** * Used when creating a new project */ diff --git a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx index b7dcef062..f4ef0400e 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx +++ b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx @@ -12,17 +12,22 @@ import { PartnerInput, AddedCoaches, } from "../../../components/ProjectsComponents/CreateProjectComponents"; +import { SkillProject } from "../../../data/interfaces/projects"; +import AddedSkills from "../../../components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills"; export default function CreateProjectPage() { const [name, setName] = useState(""); const [numberOfStudents, setNumberOfStudents] = useState(0); - const [skills, setSkills] = useState([]); const [partners, setPartners] = useState([]); // States for coaches const [coach, setCoach] = useState(""); const [coaches, setCoaches] = useState([]); + // States for skills + const [skill, setSkill] = useState(""); + const [skills, setSkills] = useState([]); + const navigate = useNavigate(); return ( @@ -48,7 +53,9 @@ export default function CreateProjectPage() { /> - + + + { @@ -56,7 +63,7 @@ export default function CreateProjectPage() { "2022", name, numberOfStudents!, - skills, + [], partners, coaches ); From 08623c1472a9c46e966be69173e31dd15f42f123 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 18 Apr 2022 12:43:19 +0200 Subject: [PATCH 479/536] Don't tell admins they should ask for an invite --- .../components/EditionsPage/EditionsTable.tsx | 9 ++--- .../EmptyEditionsTableMessage.tsx | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/EditionsPage/EmptyEditionsTableMessage.tsx diff --git a/frontend/src/components/EditionsPage/EditionsTable.tsx b/frontend/src/components/EditionsPage/EditionsTable.tsx index 885cbb70f..c4284f66b 100644 --- a/frontend/src/components/EditionsPage/EditionsTable.tsx +++ b/frontend/src/components/EditionsPage/EditionsTable.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react"; import { StyledTable, LoadingSpinner } from "./styles"; import { getEditions } from "../../utils/api/editions"; import EditionRow from "./EditionRow"; +import EmptyEditionsTableMessage from "./EmptyEditionsTableMessage"; /** * Table on the [[EditionsPage]] that renders a list of all editions @@ -34,13 +35,7 @@ export default function EditionsTable() { } if (rows.length === 0) { - return ( -
                                    - It looks like you're not a part of any editions so far. -
                                    - Contact an admin to receive an invite. -
                                    - ); + return ; } return ( diff --git a/frontend/src/components/EditionsPage/EmptyEditionsTableMessage.tsx b/frontend/src/components/EditionsPage/EmptyEditionsTableMessage.tsx new file mode 100644 index 000000000..7c54ad8b3 --- /dev/null +++ b/frontend/src/components/EditionsPage/EmptyEditionsTableMessage.tsx @@ -0,0 +1,34 @@ +import { useAuth } from "../../contexts"; +import { Role } from "../../data/enums"; +import React from "react"; + +/** + * Message shown when the [[EditionsTable]] is empty. + */ +export default function EmptyEditionsTableMessage() { + const { role } = useAuth(); + + let message: React.ReactNode; + + // Show a different message to admins & coaches + // admins are never part of any editions, coaches are never able to create an edition + if (role === Role.ADMIN) { + message = ( + <> + There are no editions yet. +
                                    + You can use the button above to create one. + + ); + } else { + message = ( + <> + It looks like you're not a part of any editions so far. +
                                    + Contact an admin to receive an invite. + + ); + } + + return
                                    {message}
                                    ; +} From 03fd1e368da575052a99e71b858bd44f520ff530 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 18 Apr 2022 12:58:03 +0200 Subject: [PATCH 480/536] Hide navlinks if no edition --- .../src/components/Navbar/EditionNavLink.tsx | 23 +++++++++++++++++++ frontend/src/components/Navbar/Navbar.tsx | 15 +++++++----- .../src/components/Navbar/UsersDropdown.tsx | 9 +++++--- 3 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 frontend/src/components/Navbar/EditionNavLink.tsx diff --git a/frontend/src/components/Navbar/EditionNavLink.tsx b/frontend/src/components/Navbar/EditionNavLink.tsx new file mode 100644 index 000000000..bb165b178 --- /dev/null +++ b/frontend/src/components/Navbar/EditionNavLink.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +interface Props { + children: React.ReactNode; + currentEdition?: string | undefined; +} + +/** + * Wrapper component for a NavLink that should only be displayed if + * the current edition is defined. + * + * This means that when the user is not in any editions yet, the link + * will be hidden. + * + * Any number of NavLinks can be included as children, it's unnecessary to wrap every + * single link with this component. + * + * An example is the link that goes to the [[StudentsPage]]. + */ +export default function EditionNavLink({ children, currentEdition }: Props) { + if (!currentEdition) return null; + return <>{children}; +} diff --git a/frontend/src/components/Navbar/Navbar.tsx b/frontend/src/components/Navbar/Navbar.tsx index 48e42f6af..e02ed3f68 100644 --- a/frontend/src/components/Navbar/Navbar.tsx +++ b/frontend/src/components/Navbar/Navbar.tsx @@ -9,6 +9,7 @@ import { matchPath, useLocation } from "react-router-dom"; import UsersDropdown from "./UsersDropdown"; import NavbarBase from "./NavbarBase"; import { LinkContainer } from "react-router-bootstrap"; +import EditionNavLink from "./EditionNavLink"; /** * Navbar component displayed at the top of the screen. @@ -63,12 +64,14 @@ export default function Navbar() { Editions - - Projects - - - Students - + + + Projects + + + Students + + diff --git a/frontend/src/components/Navbar/UsersDropdown.tsx b/frontend/src/components/Navbar/UsersDropdown.tsx index cd6ea3a3c..a2a543702 100644 --- a/frontend/src/components/Navbar/UsersDropdown.tsx +++ b/frontend/src/components/Navbar/UsersDropdown.tsx @@ -3,6 +3,7 @@ import NavDropdown from "react-bootstrap/NavDropdown"; import { StyledDropdownItem } from "./styles"; import { Role } from "../../data/enums"; import { LinkContainer } from "react-router-bootstrap"; +import EditionNavLink from "./EditionNavLink"; interface Props { currentEdition: string; @@ -26,9 +27,11 @@ export default function UsersDropdown({ currentEdition }: Props) { Admins - - Coaches - + + + Coaches + + ); } From 3e1ff42e2008e70d9ff4bd757ff4a727699e469a Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 24 Apr 2022 18:33:46 +0200 Subject: [PATCH 481/536] Adapt message --- .../src/components/EditionsPage/EmptyEditionsTableMessage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/EditionsPage/EmptyEditionsTableMessage.tsx b/frontend/src/components/EditionsPage/EmptyEditionsTableMessage.tsx index 7c54ad8b3..ea7e5a97f 100644 --- a/frontend/src/components/EditionsPage/EmptyEditionsTableMessage.tsx +++ b/frontend/src/components/EditionsPage/EmptyEditionsTableMessage.tsx @@ -23,7 +23,7 @@ export default function EmptyEditionsTableMessage() { } else { message = ( <> - It looks like you're not a part of any editions so far. + It looks like you're not a part of any editions.
                                    Contact an admin to receive an invite. From 9327bc7d27c298d3be90eaf110944cc83c40f498 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 24 Apr 2022 20:43:54 +0200 Subject: [PATCH 482/536] editions manual --- files/user_manual.md | 57 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/files/user_manual.md b/files/user_manual.md index 5ebe2221a..6e5025c94 100644 --- a/files/user_manual.md +++ b/files/user_manual.md @@ -8,17 +8,18 @@ After you have registered yourself and have been approved by one of the administ There are different ways to log in, depending on the way in which you have registered yourself. **Please note: you can only log in through the method you have registered yourself with.** -## Email +### Email + 1. Fill in your email address and password in the corresponding fields. 2. Click the "Log in" button. -## GitHub -1. Click the "Log in" button with the GitHub logo. +### GitHub -## Google -1. Click the "Log in" button with the Google logo. +1. Click the "Log in" button with the GitHub logo. +### Google +1. Click the "Log in" button with the Google logo. ## Admins @@ -68,3 +69,49 @@ Next to the email address, there is a button to remove a user as admin. Once cli 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 an remove other admins. + +## Editions + +This section contains all actions related to managing your editions. + +### Viewing a list of available editions + +In the navbar, you should see an **Editions** button. When clicked, this button brings you to "/editions", which we'll call the "Editions Page". Admins can see _all_ editions, coaches can only see the editions they're coach of. + +This page lists all editions, and contains buttons for: + +- [Creating new editions](#creating-a-new-edition-admin-only) +- [Deleting editions](#deleting-an-edition-admin-only) + +### Creating a new edition (Admin-only) + +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 + +You've now created a new edition to which you can add coaches, projects, and students. + +### Deleting an edition (Admin-only) + +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 + +**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: + +- All projects linked to this edition +- All students linked to this edition + +### Changing the current edition + +We have made a component to quickly go to a page from a previous edition. In the navbar, there's a dropdown of which the label is the name of the currently selected edition. + +_**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 + +You have now set another edition as your "current edition". This means that navigating through the navbar will show results for that specific edition. From 5ab3ae65a34693afa2dfb844c519cb88fdadfea5 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 24 Apr 2022 20:58:58 +0200 Subject: [PATCH 483/536] update module overview --- files/module_overview_frontend.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/files/module_overview_frontend.md b/files/module_overview_frontend.md index ce981ae77..d534b7f23 100644 --- a/files/module_overview_frontend.md +++ b/files/module_overview_frontend.md @@ -2,12 +2,20 @@ ## components -Here are all components we need for the frontend. Each component is in its own folder. +Here are all components we need for the frontend. Components are organized into folders that group related components together. This structure helps keeping the directory clean. -## utils/api +## contexts -These scrips make request to the API. +The contexts module contains our custom React context providers. + +## data + +This module contains our enums and interfaces. + +## utils + +This module has all functions that are not directly React-related, and a series of utility functions to make our code cleaner. This includes API requests, logic, and functions that interact with LocalStorage and SessionStorage. ## views -Here are all the views we have in the frontend. +Here are all the views (pages) we have in the frontend. Every view is a very simple component, because they are split up into smaller components that can be found in the [components](#components) module. From 1d3ff0f8b071812c4394ecfa14612525c3c37b48 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sun, 24 Apr 2022 21:42:15 +0200 Subject: [PATCH 484/536] Overwrite existing suggestions --- backend/src/app/logic/suggestions.py | 13 ++++-- .../students/suggestions/suggestions.py | 3 ++ backend/src/database/crud/suggestions.py | 16 +++++-- .../test_crud/test_suggestions.py | 42 +++++++++++++++++-- .../test_suggestions/test_suggestions.py | 25 ++++++++++- 5 files changed, 88 insertions(+), 11 deletions(-) diff --git a/backend/src/app/logic/suggestions.py b/backend/src/app/logic/suggestions.py index 6edbac25c..d2b0a6771 100644 --- a/backend/src/app/logic/suggestions.py +++ b/backend/src/app/logic/suggestions.py @@ -2,7 +2,7 @@ from src.app.schemas.suggestion import NewSuggestion from src.database.crud.suggestions import ( - create_suggestion, get_suggestions_of_student, delete_suggestion, update_suggestion) + create_suggestion, get_suggestions_of_student, get_own_suggestion, delete_suggestion, update_suggestion) from src.database.models import Suggestion, User from src.app.schemas.suggestion import SuggestionListResponse, SuggestionResponse, suggestion_model_to_schema from src.app.exceptions.authentication import MissingPermissionsException @@ -11,8 +11,15 @@ def make_new_suggestion(db: Session, new_suggestion: NewSuggestion, user: User, student_id: int | None) -> SuggestionResponse: """"Make a new suggestion""" - suggestion_orm = create_suggestion( - db, user.user_id, student_id, new_suggestion.suggestion, new_suggestion.argumentation) + own_suggestion = get_own_suggestion(db, student_id, user.user_id) + + if own_suggestion is None: + suggestion_orm = 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) + suggestion_orm = own_suggestion + suggestion = suggestion_model_to_schema(suggestion_orm) return SuggestionResponse(suggestion=suggestion) diff --git a/backend/src/app/routers/editions/students/suggestions/suggestions.py b/backend/src/app/routers/editions/students/suggestions/suggestions.py index 113b5aefb..7fa06def7 100644 --- a/backend/src/app/routers/editions/students/suggestions/suggestions.py +++ b/backend/src/app/routers/editions/students/suggestions/suggestions.py @@ -20,6 +20,9 @@ async def create_suggestion(new_suggestion: NewSuggestion, student: Student = De db: Session = Depends(get_session), user: User = Depends(require_auth)): """ 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) diff --git a/backend/src/database/crud/suggestions.py b/backend/src/database/crud/suggestions.py index 1f9a9f90a..be3853b8d 100644 --- a/backend/src/database/crud/suggestions.py +++ b/backend/src/database/crud/suggestions.py @@ -21,6 +21,16 @@ def get_suggestions_of_student(db: Session, student_id: int | None) -> list[Sugg return db.query(Suggestion).where(Suggestion.student_id == student_id).all() +def get_own_suggestion(db: Session, 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() + + def get_suggestion_by_id(db: Session, suggestion_id: int) -> Suggestion: """Give a suggestion based on the ID""" return db.query(Suggestion).where(Suggestion.suggestion_id == suggestion_id).one() @@ -42,6 +52,6 @@ def update_suggestion(db: Session, suggestion: Suggestion, decision: DecisionEnu def get_suggestions_of_student_by_type(db: Session, 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() + return db.query(Suggestion) \ + .where(Suggestion.student_id == student_id) \ + .where(Suggestion.suggestion == type_suggestion).all() diff --git a/backend/tests/test_database/test_crud/test_suggestions.py b/backend/tests/test_database/test_crud/test_suggestions.py index da2c75fbf..822663149 100644 --- a/backend/tests/test_database/test_crud/test_suggestions.py +++ b/backend/tests/test_database/test_crud/test_suggestions.py @@ -5,14 +5,13 @@ from src.database.models import Suggestion, Student, User, Edition, Skill - from src.database.crud.suggestions import (create_suggestion, get_suggestions_of_student, - get_suggestion_by_id, delete_suggestion, update_suggestion, + get_suggestion_by_id, get_own_suggestion, delete_suggestion, + update_suggestion, get_suggestions_of_student_by_type) from src.database.enums import DecisionEnum - @pytest.fixture def database_with_data(database_session: Session): """A function to fill the database with fake data that can easly be used when testing""" @@ -131,6 +130,42 @@ 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): + """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() + + suggestion = 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 + + +def test_get_own_suggestion_non_existing(database_with_data: Session): + """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() + + assert 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): + """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") + + 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 + + def test_one_coach_two_students(database_with_data: Session): """Test that one coach can write multiple suggestions""" @@ -281,4 +316,3 @@ def test_get_suggestions_of_student_by_type(database_with_data: Session): assert len(suggestions_student_yes) == 1 assert len(suggestions_student_no) == 1 assert len(suggestions_student_maybe) == 1 - 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 5c834bf7f..487af0c5c 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 @@ -57,7 +57,7 @@ def database_with_data(database_session: Session) -> Session: def test_new_suggestion(database_with_data: Session, auth_client: AuthClient): - """Tests a new sugesstion""" + """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/", @@ -72,6 +72,29 @@ def test_new_suggestion(database_with_data: Session, auth_client: AuthClient): "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"}) + + suggestions: list[Suggestion] = database_with_data.query( + Suggestion).where(Suggestion.student_id == 2).all() + assert len(suggestions) == 1 + + # 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 + + def test_new_suggestion_not_authorized(database_with_data: Session, auth_client: AuthClient): """Tests when not authorized you can't add a new suggestion""" From 005f266914e3e7594e655e603d692a85813fbb74 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Sun, 24 Apr 2022 21:56:07 +0200 Subject: [PATCH 485/536] Resolve requested changes --- frontend/src/utils/api/editions.ts | 10 ++++- .../CreateEditionPage/CreateEditionPage.tsx | 39 +++++++++++-------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/frontend/src/utils/api/editions.ts b/frontend/src/utils/api/editions.ts index f85e297f8..98d7bdc16 100644 --- a/frontend/src/utils/api/editions.ts +++ b/frontend/src/utils/api/editions.ts @@ -19,6 +19,14 @@ export async function getEditions(): Promise { return response.data as EditionsResponse; } +/** + * Get all edition names sorted the user can see + */ +export async function getSortedEditions(): Promise { + const response = await axiosInstance.get("/users/current"); + return response.data.editions; +} + /** * Delete an edition by name */ @@ -39,7 +47,7 @@ export async function createEdition(name: string, year: number): Promise if (axios.isAxiosError(error) && error.response !== undefined) { return error.response.status; } else { - return -1; + throw error; } } } diff --git a/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx b/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx index 4f0d9651f..7a18c7110 100644 --- a/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx +++ b/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx @@ -1,14 +1,17 @@ import { Button, Form, Spinner } from "react-bootstrap"; -import { SyntheticEvent, useState } from "react"; -import { createEdition } from "../../utils/api/editions"; +import React, { 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 { useAuth } from "../../contexts"; /** * Page to create a new edition. */ export default function CreateEditionPage() { const navigate = useNavigate(); + const { setEditions } = useAuth(); + const currentYear = new Date().getFullYear(); const [name, setName] = useState(""); @@ -18,12 +21,12 @@ export default function CreateEditionPage() { const [error, setError] = useState(undefined); const [loading, setLoading] = useState(false); - async function sendEdition(name: string, year: number) { + async function sendEdition(name: string, year: number): Promise { const response = await createEdition(name, year); - setLoading(false); - let success = false; if (response === 201) { - success = true; + const allEditions = await getSortedEditions(); + setEditions(allEditions); + return true; } else if (response === 409) { setNameError("Edition name already exists."); } else if (response === 422) { @@ -31,13 +34,12 @@ export default function CreateEditionPage() { } else { setError("Something went wrong."); } - if (success) { - // navigate must be at the end of the function - navigate("/editions/"); - } + return false; } - const handleSubmit = (event: SyntheticEvent) => { + async function handleSubmit(event: SyntheticEvent) { + event.stopPropagation(); + event.preventDefault(); let correct = true; // Edition name can't contain spaces and must be at least 5 long. @@ -45,7 +47,7 @@ export default function CreateEditionPage() { if (name.includes(" ")) { setNameError("Edition name can't contain spaces."); } else if (name.length < 5) { - setNameError("Edition name must be longer than 4."); + setNameError("Edition name must be longer than 4 characters."); } else { setNameError("Invalid edition name."); } @@ -66,13 +68,18 @@ export default function CreateEditionPage() { } } + let success = false; if (correct) { setLoading(true); - sendEdition(name, yearNumber); + success = await sendEdition(name, yearNumber); + setLoading(false); } - event.preventDefault(); - event.stopPropagation(); - }; + + if (success) { + // navigate must be at the end of the function + navigate("/editions/"); + } + } let submitButton; if (loading) { From cce4e41db1eb2124cc9794207d371fada84d6815 Mon Sep 17 00:00:00 2001 From: Seppe <57944475+SeppeM8@users.noreply.github.com> Date: Sun, 24 Apr 2022 22:01:38 +0200 Subject: [PATCH 486/536] Apply suggestions from code review Co-authored-by: Stijn De Clercq <60451863+stijndcl@users.noreply.github.com> --- files/user_manual.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/files/user_manual.md b/files/user_manual.md index 5ebe2221a..448194a92 100644 --- a/files/user_manual.md +++ b/files/user_manual.md @@ -39,15 +39,15 @@ At the top left, you can invite someone via an invite link. You can choose betwe #### Requests -At the top middle of the page, you find a dropdown with **Requests**. When you expend the requests, you see a list of all user requests. This are all users who used an invite link to create an account. +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 his 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 searching a name. Each row contains the name and email 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, 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. #### Coaches -A the centre of the page, you find a list of all users who are coach in the current edition. As in the request list, each row contains the name and email address of a user. The list can be filtered by searching a name. +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. 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: @@ -67,4 +67,4 @@ Next to the email address, there is a button to remove a user as admin. Once cli 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 an remove other admins. +**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. From e3937009fb9839218d65702e6526ac0bf826c6cc Mon Sep 17 00:00:00 2001 From: Seppe <57944475+SeppeM8@users.noreply.github.com> Date: Sun, 24 Apr 2022 22:02:35 +0200 Subject: [PATCH 487/536] Update user_manual.md --- files/user_manual.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/user_manual.md b/files/user_manual.md index 448194a92..b3aa9c8c3 100644 --- a/files/user_manual.md +++ b/files/user_manual.md @@ -51,7 +51,7 @@ A the centre of the page, you can find a list of all users who are coach in the 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 a coach, he won't be able to see any data from any edition anymore +- **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. 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. From 143a1142d7a37d4c42e0a083718442fbfbf13536 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Mon, 25 Apr 2022 12:20:43 +0200 Subject: [PATCH 488/536] tests to filter on emails --- backend/src/app/logic/students.py | 4 +- .../app/routers/editions/students/students.py | 2 +- backend/src/database/crud/students.py | 4 +- backend/src/database/enums.py | 2 +- .../test_students/test_students.py | 46 ++++++++++++++++--- 5 files changed, 46 insertions(+), 12 deletions(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index aa3381cc0..634e6fc44 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -1,8 +1,6 @@ -from re import S from sqlalchemy.orm import Session from sqlalchemy.orm.exc import NoResultFound -from src.app.exceptions.students_email import FailedToAddNewEmailException 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, @@ -15,7 +13,7 @@ from src.app.schemas.students import ( ReturnStudentList, ReturnStudent, CommonQueryParams, ReturnStudentMailList, Student as StudentModel, Suggestions as SuggestionsModel, - NewEmail, DecisionEmail as DecionEmailModel, EmailsSearchQueryParams, + NewEmail, EmailsSearchQueryParams, ListReturnStudentMailList) diff --git a/backend/src/app/routers/editions/students/students.py b/backend/src/app/routers/editions/students/students.py index 323d99743..076b43222 100644 --- a/backend/src/app/routers/editions/students/students.py +++ b/backend/src/app/routers/editions/students/students.py @@ -9,7 +9,7 @@ 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, DecisionEmail, NewEmail, EmailsSearchQueryParams, + ReturnStudentMailList, NewEmail, EmailsSearchQueryParams, ListReturnStudentMailList) from src.database.database import get_session from src.database.models import Student, Edition diff --git a/backend/src/database/crud/students.py b/backend/src/database/crud/students.py index a77a4e9c0..cd3d44525 100644 --- a/backend/src/database/crud/students.py +++ b/backend/src/database/crud/students.py @@ -75,7 +75,9 @@ def get_last_emails_of_students(db: Session, edition: Edition, commons: EmailsSe emails = db.query(DecisionEmail).join( last_emails, DecisionEmail.email_id == last_emails.c.email_id ) - if commons.email_status: + + # it has to be here, otherwise it skips it when it's 0 (APPROVED) + if commons.email_status is not None: emails = emails.where(DecisionEmail.decision == commons.email_status) emails = emails.order_by(DecisionEmail.student_id) diff --git a/backend/src/database/enums.py b/backend/src/database/enums.py index ec76d63d6..5af6831a3 100644 --- a/backend/src/database/enums.py +++ b/backend/src/database/enums.py @@ -15,7 +15,7 @@ class DecisionEnum(enum.Enum): @enum.unique -class EmailStatusEnum(enum.Enum): +class EmailStatusEnum(enum.IntEnum): """Enum for the status attached to an email sent to a student""" # Nothing happened (undecided/screening) APPLIED = 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 602594f4a..28ae82567 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 @@ -45,11 +45,6 @@ def database_with_data(database_session: Session) -> Session: database_session.add(student30) 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() return database_session @@ -394,6 +389,8 @@ def test_get_emails_student_coach(database_with_data: Session, auth_client: Auth def test_get_emails_student_admin(database_with_data: Session, 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 @@ -522,7 +519,7 @@ def test_get_emails(database_with_data: Session, auth_client: AuthClient): """test get emails""" auth_client.admin() response = auth_client.post("/editions/ed2022/students/emails", - json={"students_id": [1], "email_status": 3}) + 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") @@ -532,3 +529,40 @@ def test_get_emails(database_with_data: Session, auth_client: AuthClient): assert response.json()["studentEmails"][0]["emails"][0]["decision"] == 3 assert response.json()["studentEmails"][1]["student"]["studentId"] == 2 assert response.json()["studentEmails"][1]["emails"][0]["decision"] == 5 + + +def test_emails_filter_first_name(database_with_data: Session, 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}) + response = auth_client.get( + "/editions/ed2022/students/emails/?first_name=Jos") + 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): + """test get emails with filter last name""" + auth_client.admin() + auth_client.post("/editions/ed2022/students/emails", + json={"students_id": [1], "email_status": 1}) + response = auth_client.get( + "/editions/ed2022/students/emails/?last_name=Vermeulen") + assert len(response.json()["studentEmails"]) == 1 + assert response.json()[ + "studentEmails"][0]["student"]["lastName"] == "Vermeulen" + + +def test_emails_filter_emailstatus(database_with_data: Session, 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( + "/editions/ed2022/students/emails/?email_status="+str(i)) + assert len(response.json()["studentEmails"]) == 1 + if i > 0: + response = auth_client.get( + "/editions/ed2022/students/emails/?email_status="+str(i-1)) From d0c6e71758538d4c3dee6e1e6e25235e864d9035 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Mon, 25 Apr 2022 13:28:03 +0200 Subject: [PATCH 489/536] all bert changes are in it --- backend/src/app/schemas/students.py | 2 +- backend/src/database/crud/students.py | 5 ++- .../test_database/test_crud/test_students.py | 35 +++++++++++++++---- .../test_students/test_students.py | 20 +++++++++-- 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index fcbb5ffdf..5cc4fc6ab 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -77,7 +77,7 @@ class EmailsSearchQueryParams: """search query paramaters for email""" first_name: str = "" last_name: str = "" - email_status: EmailStatusEnum | None = None + email_status: list[EmailStatusEnum] = Query([]) page: int = 0 diff --git a/backend/src/database/crud/students.py b/backend/src/database/crud/students.py index cd3d44525..40a22f591 100644 --- a/backend/src/database/crud/students.py +++ b/backend/src/database/crud/students.py @@ -76,9 +76,8 @@ def get_last_emails_of_students(db: Session, edition: Edition, commons: EmailsSe last_emails, DecisionEmail.email_id == last_emails.c.email_id ) - # it has to be here, otherwise it skips it when it's 0 (APPROVED) - if commons.email_status is not None: - emails = emails.where(DecisionEmail.decision == commons.email_status) + 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() diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index 91cf5a447..863ecf6fd 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -282,7 +282,7 @@ def test_get_last_emails_of_students_filter_applied(database_with_data: Session) create_email(database_with_data, student1, EmailStatusEnum.CONTRACT_CONFIRMED) emails: list[DecisionEmail] = get_last_emails_of_students( - database_with_data, edition, EmailsSearchQueryParams(email_status=EmailStatusEnum.APPLIED)) + database_with_data, edition, EmailsSearchQueryParams(email_status=[EmailStatusEnum.APPLIED])) assert len(emails) == 1 assert emails[0].student_id == 2 @@ -301,7 +301,7 @@ def test_get_last_emails_of_students_filter_awaiting_project(database_with_data: create_email(database_with_data, student1, EmailStatusEnum.CONTRACT_CONFIRMED) emails: list[DecisionEmail] = get_last_emails_of_students( - database_with_data, edition, EmailsSearchQueryParams(email_status=EmailStatusEnum.AWAITING_PROJECT)) + database_with_data, edition, EmailsSearchQueryParams(email_status=[EmailStatusEnum.AWAITING_PROJECT])) assert len(emails) == 1 assert emails[0].student_id == 2 @@ -320,7 +320,7 @@ def test_get_last_emails_of_students_filter_approved(database_with_data: Session create_email(database_with_data, student1, EmailStatusEnum.CONTRACT_CONFIRMED) emails: list[DecisionEmail] = get_last_emails_of_students( - database_with_data, edition, EmailsSearchQueryParams(email_status=EmailStatusEnum.APPROVED)) + database_with_data, edition, EmailsSearchQueryParams(email_status=[EmailStatusEnum.APPROVED])) assert len(emails) == 1 assert emails[0].student_id == 2 @@ -337,7 +337,7 @@ def test_get_last_emails_of_students_filter_contract_confirmed(database_with_dat create_email(database_with_data, student2, EmailStatusEnum.CONTRACT_CONFIRMED) emails: list[DecisionEmail] = get_last_emails_of_students( - database_with_data, edition, EmailsSearchQueryParams(email_status=EmailStatusEnum.CONTRACT_CONFIRMED)) + database_with_data, edition, EmailsSearchQueryParams(email_status=[EmailStatusEnum.CONTRACT_CONFIRMED])) assert len(emails) == 1 assert emails[0].student_id == 2 @@ -356,7 +356,7 @@ def test_get_last_emails_of_students_filter_contract_declined(database_with_data create_email(database_with_data, student1, EmailStatusEnum.CONTRACT_CONFIRMED) emails: list[DecisionEmail] = get_last_emails_of_students( - database_with_data, edition, EmailsSearchQueryParams(email_status=EmailStatusEnum.CONTRACT_DECLINED)) + database_with_data, edition, EmailsSearchQueryParams(email_status=[EmailStatusEnum.CONTRACT_DECLINED])) assert len(emails) == 1 assert emails[0].student_id == 2 @@ -375,7 +375,7 @@ def test_get_last_emails_of_students_filter_rejected(database_with_data: Session create_email(database_with_data, student1, EmailStatusEnum.CONTRACT_CONFIRMED) emails: list[DecisionEmail] = get_last_emails_of_students( - database_with_data, edition, EmailsSearchQueryParams(email_status=EmailStatusEnum.REJECTED)) + database_with_data, edition, EmailsSearchQueryParams(email_status=[EmailStatusEnum.REJECTED])) assert len(emails) == 1 assert emails[0].student_id == 2 @@ -418,3 +418,26 @@ def test_get_last_emails_of_students_last_name(database_with_data: Session): assert len(emails) == 1 assert emails[0].student_id == 1 assert emails[0].decision == EmailStatusEnum.CONTRACT_CONFIRMED + + +def test_get_last_emails_of_students_filter_mutliple_status(database_with_data: Session): + """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( + database_with_data, edition, EmailsSearchQueryParams(email_status= + [ + EmailStatusEnum.APPLIED, + EmailStatusEnum.CONTRACT_CONFIRMED + ])) + + assert len(emails) == 2 + assert emails[0].student_id == 1 + assert emails[0].decision == EmailStatusEnum.CONTRACT_CONFIRMED + assert emails[1].student_id == 2 + assert emails[1].decision == EmailStatusEnum.APPLIED 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 28ae82567..a1d01fe46 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 @@ -561,8 +561,24 @@ def test_emails_filter_emailstatus(database_with_data: Session, auth_client: Aut auth_client.post("/editions/ed2022/students/emails", json={"students_id": [2], "email_status": i}) response = auth_client.get( - "/editions/ed2022/students/emails/?email_status="+str(i)) + f"/editions/ed2022/students/emails/?email_status={i}") + print(response.json()) assert len(response.json()["studentEmails"]) == 1 if i > 0: response = auth_client.get( - "/editions/ed2022/students/emails/?email_status="+str(i-1)) + 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): + """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") + assert len(response.json()["studentEmails"]) == 2 + assert response.json()["studentEmails"][0]["student"]["studentId"] == 1 + assert response.json()["studentEmails"][1]["student"]["studentId"] == 2 From cbaa6a6a936f80b6585ea088c99040f6f6ae8ab8 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Mon, 25 Apr 2022 14:00:20 +0200 Subject: [PATCH 490/536] Added some style --- .../AddedCoaches/AddedCoaches.tsx | 1 + .../AddedSkills/AddedSkills.tsx | 90 ++++++++++--------- .../AddedSkills/styles.ts | 41 +++++++++ .../InputFields/Skill/Skill.tsx | 20 ++--- 4 files changed, 101 insertions(+), 51 deletions(-) create mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/styles.ts diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx index 4791f30b2..9fe44734b 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx @@ -21,6 +21,7 @@ export default function AddedCoaches({ }} > + ))} diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills.tsx index 06fa1ef40..d29baa8d4 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills.tsx @@ -1,4 +1,7 @@ import { SkillProject } from "../../../../data/interfaces/projects"; +import { Input } from "../styles"; +import { AmountInput, SkillContainer, DescriptionContainer, Delete, TopContainer } from "./styles"; +import { TiDeleteOutline } from "react-icons/ti"; export default function AddedSkills({ skills, @@ -10,48 +13,55 @@ export default function AddedSkills({ return (
                                    {skills.map((skill, _index) => ( -
                                    - {skill.skill} - { - const newList = skills.map((item, otherIndex) => { - if (_index === otherIndex) { - const updatedItem = { - ...item, - amount: event.target.valueAsNumber, - }; - return updatedItem; - } - return item; - }); + + + {skill.skill} + { + const newList = skills.map((item, otherIndex) => { + if (_index === otherIndex) { + const updatedItem = { + ...item, + amount: event.target.valueAsNumber, + }; + return updatedItem; + } + return item; + }); - setSkills(newList); - }} - /> - { - const newList = skills.map((item, otherIndex) => { - if (_index === otherIndex) { - const updatedItem = { - ...item, - description: event.target.value, - }; - return updatedItem; - } - return item; - }); + setSkills(newList); + }} + /> + + + + + + { + const newList = skills.map((item, otherIndex) => { + if (_index === otherIndex) { + const updatedItem = { + ...item, + description: event.target.value, + }; + return updatedItem; + } + return item; + }); - setSkills(newList); - }} - /> -
                                    + setSkills(newList); + }} + /> + + ))}
                                    ); diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/styles.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/styles.ts new file mode 100644 index 000000000..218d26fa5 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/styles.ts @@ -0,0 +1,41 @@ +import styled from "styled-components"; + +export const SkillContainer = styled.div` + border-radius: 5px; + margin-top: 10px; + margin-left: 5px; + background-color: #1a1a36; + padding: 5px 10px; + width: fit-content; +`; + +export const TopContainer = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + +export const Delete = styled.button` + background-color: #f14a3b; + border: 0; + padding: 2.5px 2.5px; + border-radius: 1px; + color: white; + display: flex; + align-items: center; +`; + +export const DescriptionContainer = styled.div` + margin-bottom: 10px; +`; + +export const AmountInput = styled.input` + margin: 5px; + padding: 2px 10px; + background-color: #131329; + color: white; + border: none; + border-radius: 5px; + + width: 100px; +`; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx index 1746ab745..5d4124b47 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx @@ -30,19 +30,17 @@ export default function Skill({ { - if (availableSkills.some(availableSkill => availableSkill === skill)) { - if (!skills.some(existingSkill => existingSkill.skill === skill)) { - const newSkills = [...skills]; - const newSkill: SkillProject = { - skill: skill, - description: undefined, - amount: undefined, - }; - newSkills.push(newSkill); - setSkills(newSkills); - } + const newSkills = [...skills]; + const newSkill: SkillProject = { + skill: skill, + description: undefined, + amount: undefined, + }; + newSkills.push(newSkill); + setSkills(newSkills); } + setSkill(""); }} > Add skill From 31397f373d61b4a213d91d67f04c35da778ea867 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Mon, 25 Apr 2022 14:04:24 +0200 Subject: [PATCH 491/536] change grid layout --- frontend/src/views/projectViews/ProjectsPage/styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/projectViews/ProjectsPage/styles.ts b/frontend/src/views/projectViews/ProjectsPage/styles.ts index ddbe9537b..75f3b4804 100644 --- a/frontend/src/views/projectViews/ProjectsPage/styles.ts +++ b/frontend/src/views/projectViews/ProjectsPage/styles.ts @@ -4,7 +4,7 @@ import { Form } from "react-bootstrap"; export const CardsGrid = styled.div` display: grid; grid-gap: 5px; - grid-template-columns: repeat(auto-fit, minmax(600px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(375px, 1fr)); grid-auto-flow: dense; `; From 45116ee77151e67ddb46c56a4e4255b5e5baf31c Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Mon, 25 Apr 2022 14:35:42 +0200 Subject: [PATCH 492/536] fixed tests --- backend/tests/test_database/test_crud/test_students.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index 863ecf6fd..637dc0725 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -264,7 +264,7 @@ def test_get_last_emails_of_students(database_with_data: Session): EmailStatusEnum.REJECTED) emails: list[DecisionEmail] = get_last_emails_of_students( - database_with_data, edition, EmailsSearchQueryParams()) + database_with_data, edition, EmailsSearchQueryParams(email_status=[])) assert len(emails) == 2 assert emails[0].student_id == 1 assert emails[0].decision == EmailStatusEnum.CONTRACT_CONFIRMED @@ -394,7 +394,7 @@ def test_get_last_emails_of_students_first_name(database_with_data: Session): create_email(database_with_data, student1, EmailStatusEnum.CONTRACT_CONFIRMED) emails: list[DecisionEmail] = get_last_emails_of_students( - database_with_data, edition, EmailsSearchQueryParams(first_name="Jos")) + database_with_data, edition, EmailsSearchQueryParams(first_name="Jos", email_status=[])) assert len(emails) == 1 assert emails[0].student_id == 1 @@ -413,7 +413,7 @@ def test_get_last_emails_of_students_last_name(database_with_data: Session): create_email(database_with_data, student1, EmailStatusEnum.CONTRACT_CONFIRMED) emails: list[DecisionEmail] = get_last_emails_of_students( - database_with_data, edition, EmailsSearchQueryParams(last_name="Vermeulen")) + database_with_data, edition, EmailsSearchQueryParams(last_name="Vermeulen", email_status=[])) assert len(emails) == 1 assert emails[0].student_id == 1 From fd8950b37ab3dcc751b85478ce6e8ced73be50ab Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Mon, 25 Apr 2022 14:53:18 +0200 Subject: [PATCH 493/536] type error fix --- backend/src/app/logic/students.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/app/logic/students.py b/backend/src/app/logic/students.py index 634e6fc44..d506fbe8d 100644 --- a/backend/src/app/logic/students.py +++ b/backend/src/app/logic/students.py @@ -98,7 +98,6 @@ def last_emails_of_students(db: Session, edition: Edition, db, edition, commons) student_emails: list[ReturnStudentMailList] = [] for email in emails: - student=get_student_by_id(db, email.student_id) - student_emails.append(ReturnStudentMailList(student=student, + student_emails.append(ReturnStudentMailList(student=email.student, emails=[email])) return ListReturnStudentMailList(student_emails=student_emails) From caf4ca308b5b759077245542b809826ceed5d77b Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Mon, 25 Apr 2022 16:58:46 +0200 Subject: [PATCH 494/536] prettier... --- frontend/src/utils/api/projects.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index a5fbe3aa8..c5abd5eaa 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -4,7 +4,9 @@ import { axiosInstance } from "./api"; export async function getProjects(edition: string, page: number) { try { - const response = await axiosInstance.get("/editions/" + edition + "/projects/?page=" + page.toString()); + const response = await axiosInstance.get( + "/editions/" + edition + "/projects/?page=" + page.toString() + ); const projects = response.data as Projects; return projects; } catch (error) { From 068c80bb95b97ef1236b26800be3658152590c0a Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Mon, 25 Apr 2022 17:31:28 +0200 Subject: [PATCH 495/536] Reused a component for added data to a list Added labels above each segment --- .../AddedCoaches/AddedCoaches.tsx | 30 ----------------- .../AddedCoaches/index.ts | 1 - .../AddedItems/AddedItems.tsx | 29 +++++++++++++++++ .../AddedItems/index.ts | 1 + .../AddedSkills/index.ts | 1 + .../AddedSkills/styles.ts | 1 - .../InputFields/Partner/Partner.tsx | 32 +++++++++++++++++-- .../InputFields/index.ts | 2 +- .../CreateProjectComponents/index.ts | 3 +- .../CreateProjectComponents/styles.ts | 3 +- .../CreateProjectPage/CreateProjectPage.tsx | 27 ++++++++++++---- .../projectViews/CreateProjectPage/styles.ts | 5 ++- 12 files changed, 91 insertions(+), 44 deletions(-) delete mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx delete mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/index.ts create mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/AddedItems.tsx create mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/index.ts create mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/index.ts diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx deleted file mode 100644 index 9fe44734b..000000000 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { TiDeleteOutline } from "react-icons/ti"; -import { AddedCoach, RemoveButton } from "../styles"; - -export default function AddedCoaches({ - coaches, - setCoaches, -}: { - coaches: string[]; - setCoaches: (coaches: string[]) => void; -}) { - return ( -
                                    - {coaches.map((element, _index) => ( - - {element} - { - const newCoaches = [...coaches]; - newCoaches.splice(_index, 1); - setCoaches(newCoaches); - }} - > - - - - - ))} -
                                    - ); -} diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/index.ts deleted file mode 100644 index fe048b5a7..000000000 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./AddedCoaches"; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/AddedItems.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/AddedItems.tsx new file mode 100644 index 000000000..9dbc6f551 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/AddedItems.tsx @@ -0,0 +1,29 @@ +import { TiDeleteOutline } from "react-icons/ti"; +import { AddedItem, RemoveButton } from "../styles"; + +export default function AddedItems({ + items, + setItems, +}: { + items: string[]; + setItems: (items: string[]) => void; +}) { + return ( +
                                    + {items.map((element, _index) => ( + + {element} + { + const newItems = [...items]; + newItems.splice(_index, 1); + setItems(newItems); + }} + > + + + + ))} +
                                    + ); +} diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/index.ts new file mode 100644 index 000000000..4c09ff358 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/index.ts @@ -0,0 +1 @@ +export { default } from "./AddedItems"; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/index.ts new file mode 100644 index 000000000..f3661f314 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/index.ts @@ -0,0 +1 @@ +export { default } from "./AddedSkills"; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/styles.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/styles.ts index 218d26fa5..6e1ed21be 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/styles.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/styles.ts @@ -3,7 +3,6 @@ import styled from "styled-components"; export const SkillContainer = styled.div` border-radius: 5px; margin-top: 10px; - margin-left: 5px; background-color: #1a1a36; padding: 5px 10px; width: fit-content; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx index 385bae14d..29cdc1d97 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx @@ -1,16 +1,44 @@ import { Input, AddButton } from "../../styles"; export default function Partner({ + partner, + setPartner, partners, setPartners, }: { + partner: string; + setPartner: (partner: string) => void; partners: string[]; setPartners: (partners: string[]) => void; }) { + const availablePartners = ["partner1", "partner2"]; // TODO get partners from API call + return (
                                    - setPartners([])} placeholder="Partner" /> - Add partner + setPartner(e.target.value)} + list="partners" + placeholder="Partner" + /> + + + {availablePartners.map((availablePartner, _index) => { + return + + { + if (!partners.includes(partner)) { + const newPartners = [...partners]; + newPartners.push(partner); + setPartners(newPartners); + } setPartner("") + }} + > + Add partner +
                                    ); } diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts index b0235977d..989b56347 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts @@ -2,4 +2,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"; +export { default as PartnerInput } from "./Partner"; \ No newline at end of file diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts index 184b4049a..939c037ec 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts @@ -5,4 +5,5 @@ export { SkillInput, PartnerInput, } from "./InputFields"; -export { default as AddedCoaches } from "./AddedCoaches"; +export { default as AddedItems } from "./AddedItems"; +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 8d199ae87..8c73747c4 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts @@ -29,8 +29,9 @@ export const RemoveButton = styled.button` align-items: center; `; -export const AddedCoach = styled.div` +export const AddedItem = styled.div` margin: 5px; + margin-left: 0; padding: 5px; background-color: #1a1a36; max-width: fit-content; diff --git a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx index f4ef0400e..00d0e9f7d 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx +++ b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx @@ -1,4 +1,4 @@ -import { CreateProjectContainer, CreateButton } from "./styles"; +import { CreateProjectContainer, CreateButton, Label } from "./styles"; import { createProject } from "../../../utils/api/projects"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -10,15 +10,14 @@ import { CoachInput, SkillInput, PartnerInput, - AddedCoaches, + AddedItems, + AddedSkills, } from "../../../components/ProjectsComponents/CreateProjectComponents"; import { SkillProject } from "../../../data/interfaces/projects"; -import AddedSkills from "../../../components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills"; export default function CreateProjectPage() { const [name, setName] = useState(""); const [numberOfStudents, setNumberOfStudents] = useState(0); - const [partners, setPartners] = useState([]); // States for coaches const [coach, setCoach] = useState(""); @@ -28,6 +27,10 @@ export default function CreateProjectPage() { const [skill, setSkill] = useState(""); const [skills, setSkills] = useState([]); + // States for partners + const [partner, setPartner] = useState(""); + const [partners, setPartners] = useState([]); + const navigate = useNavigate(); return ( @@ -38,25 +41,37 @@ export default function CreateProjectPage() {

                                    New Project

                                    + + + - + + - + + + + { const response = await createProject( diff --git a/frontend/src/views/projectViews/CreateProjectPage/styles.ts b/frontend/src/views/projectViews/CreateProjectPage/styles.ts index dae106a68..d924e3d74 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/styles.ts +++ b/frontend/src/views/projectViews/CreateProjectPage/styles.ts @@ -42,4 +42,7 @@ export const CreateButton = styled.button` border-radius: 5px; `; - +export const Label = styled.h5` + margin-top: 20px; + margin-bottom: 0px; +` From 3dae5faa0ea9c7f6b5e189dcfcd7970a19f1e06a Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Mon, 25 Apr 2022 17:56:33 +0200 Subject: [PATCH 496/536] project search coach is better written --- backend/src/database/crud/projects.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/database/crud/projects.py b/backend/src/database/crud/projects.py index bd97f61f3..68ac90f66 100644 --- a/backend/src/database/crud/projects.py +++ b/backend/src/database/crud/projects.py @@ -20,11 +20,11 @@ def get_projects_for_edition_page(db: Session, edition: Edition, """Returns a paginated list of all projects from a certain edition from the database""" query = _get_projects_for_edition_query(db, 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() - if not search_params.coach: - return projects - return list(set(projects) & set(user.projects)) + return projects def add_project(db: Session, edition: Edition, input_project: InputProject) -> Project: From 4c3c067b572b13e1d273b7a0b4eac5c687807eae Mon Sep 17 00:00:00 2001 From: stijndcl Date: Mon, 25 Apr 2022 22:31:39 +0200 Subject: [PATCH 497/536] Fix decision email migration --- .../versions/43e6e98fe039_create_enum_for_email_statuses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/migrations/versions/43e6e98fe039_create_enum_for_email_statuses.py b/backend/migrations/versions/43e6e98fe039_create_enum_for_email_statuses.py index 8973102ab..f51d5c0aa 100644 --- a/backend/migrations/versions/43e6e98fe039_create_enum_for_email_statuses.py +++ b/backend/migrations/versions/43e6e98fe039_create_enum_for_email_statuses.py @@ -22,12 +22,12 @@ 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) + 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) + batch_op.alter_column("decision", type_=old_type, existing_type=new_type, nullable=False) # ### end Alembic commands ### From 1b340b8590babd9f6a31e68f9465c2c995087249 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Mon, 25 Apr 2022 22:33:10 +0200 Subject: [PATCH 498/536] Move @types dependency to dev --- frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 7ffe2f3d1..a5e5e0e0a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,6 @@ "@fortawesome/fontawesome-svg-core": "^1.3.0", "@fortawesome/free-solid-svg-icons": "^6.0.0", "@fortawesome/react-fontawesome": "^0.1.17", - "@types/react-infinite-scroller": "^1.2.3", "axios": "^0.26.1", "bootstrap": "5.1.3", "buffer": "^6.0.3", @@ -37,6 +36,7 @@ "@types/node": "^16.7.13", "@types/react": "^17.0.20", "@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", From 29ad43cd9bd511a25bd5ab212527df6459fce0e9 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Mon, 25 Apr 2022 22:59:17 +0200 Subject: [PATCH 499/536] Put authType in enum --- .../GeneralComponents/AuthTypeIcon.tsx | 20 +++++++++---------- frontend/src/utils/api/users/users.ts | 12 ++++++++++- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx b/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx index dcbe28834..7bf4af1c7 100644 --- a/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx +++ b/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx @@ -1,18 +1,18 @@ import { HiOutlineMail } from "react-icons/hi"; import { AiFillGithub, AiFillGoogleCircle, AiOutlineQuestionCircle } from "react-icons/ai"; +import { AuthType } from "../../utils/api/users/users"; /** * An icon representing the type of authentication - * @param props.type email/github/google */ -export default function AuthTypeIcon(props: { type: string }) { - if (props.type === "email") { - return ; - } else if (props.type === "github") { - return ; - } else if (props.type === "google") { - return ; - } else { - return ; +export default function AuthTypeIcon(props: { type: AuthType }) { + switch (props.type) { + case AuthType.Email: + return ; + case AuthType.GitHub: + return ; + case AuthType.Google: + return ; } + return ; } diff --git a/frontend/src/utils/api/users/users.ts b/frontend/src/utils/api/users/users.ts index d2b34b21e..339399786 100644 --- a/frontend/src/utils/api/users/users.ts +++ b/frontend/src/utils/api/users/users.ts @@ -1,5 +1,14 @@ import { axiosInstance } from "../api"; +/** + * Enum for the different types of authentication. + */ +export enum AuthType { + Email = "email", + GitHub = "github", + Google = "google", +} + /** * Interface of a user. */ @@ -8,7 +17,7 @@ export interface User { name: string; admin: boolean; auth: { - authType: string; + authType: AuthType; email: string; }; } @@ -53,6 +62,7 @@ export async function getUsersExcludeEdition( const response = await axiosInstance.get( `/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}`); From 442bfad1ff9bc38ceb5e213064a9a2360ee57f5d Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Tue, 26 Apr 2022 17:05:30 +0200 Subject: [PATCH 500/536] filter on concat of first name and last name --- backend/src/app/schemas/students.py | 3 +-- backend/src/database/crud/students.py | 3 +-- .../tests/test_database/test_crud/test_students.py | 4 ++-- .../test_editions/test_students/test_students.py | 12 ++++++------ 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index 5cc4fc6ab..7248b8b27 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -64,8 +64,7 @@ class ReturnStudentList(CamelCaseModel): @dataclass class CommonQueryParams: """search query paramaters""" - first_name: str = "" - last_name: str = "" + name: str = "" alumni: bool = False student_coach: bool = False skill_ids: list[int] = Query([]) diff --git a/backend/src/database/crud/students.py b/backend/src/database/crud/students.py index 40a22f591..956039d7f 100644 --- a/backend/src/database/crud/students.py +++ b/backend/src/database/crud/students.py @@ -31,8 +31,7 @@ def get_students(db: Session, edition: Edition, """Get students""" query = db.query(Student)\ .where(Student.edition == edition)\ - .where(Student.first_name.contains(commons.first_name))\ - .where(Student.last_name.contains(commons.last_name))\ + .where((Student.first_name + ' ' + Student.last_name).contains(commons.name))\ if commons.alumni: query = query.where(Student.alumni) diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index 637dc0725..76a5e8d46 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -123,7 +123,7 @@ def test_search_students_on_first_name(database_with_data: Session): edition: Edition = database_with_data.query( Edition).where(Edition.edition_id == 1).one() students = get_students(database_with_data, edition, - CommonQueryParams(first_name="Jos")) + CommonQueryParams(name="Jos")) assert len(students) == 1 @@ -132,7 +132,7 @@ def test_search_students_on_last_name(database_with_data: Session): edition: Edition = database_with_data.query( Edition).where(Edition.edition_id == 1).one() students = get_students(database_with_data, edition, - CommonQueryParams(last_name="Vermeulen")) + CommonQueryParams(name="Vermeulen")) assert len(students) == 1 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 a1d01fe46..36511300a 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 @@ -205,7 +205,7 @@ def test_get_first_name_students(database_with_data: Session, auth_client: AuthC """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/?first_name=Jos") + response = auth_client.get("/editions/ed2022/students/?name=Jos") assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 1 @@ -221,11 +221,11 @@ def test_get_first_name_student_pagination(database_with_data: Session, auth_cli database_with_data.add(student) database_with_data.commit() response = auth_client.get( - "/editions/ed2022/students/?first_name=Student&page=0") + "/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/?first_name=Student&page=1") + "/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) @@ -236,7 +236,7 @@ def test_get_last_name_students(database_with_data: Session, auth_client: AuthCl edition: Edition = database_with_data.query(Edition).all()[0] auth_client.coach(edition) response = auth_client.get( - "/editions/ed2022/students/?last_name=Vermeulen") + "/editions/ed2022/students/?name=Vermeulen") assert response.status_code == status.HTTP_200_OK assert len(response.json()["students"]) == 1 @@ -252,11 +252,11 @@ def test_get_last_name_students_pagination(database_with_data: Session, auth_cli database_with_data.add(student) database_with_data.commit() response = auth_client.get( - "/editions/ed2022/students/?last_name=Student&page=0") + "/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/?last_name=Student&page=1") + "/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) From 56b40d5f4b1eef4598afb20254da05f438dc4e98 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Tue, 26 Apr 2022 17:09:42 +0200 Subject: [PATCH 501/536] filter emails on concat first name and last name --- backend/src/app/schemas/students.py | 3 +-- backend/src/database/crud/students.py | 3 +-- backend/tests/test_database/test_crud/test_students.py | 4 ++-- .../test_editions/test_students/test_students.py | 8 ++++++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/src/app/schemas/students.py b/backend/src/app/schemas/students.py index 7248b8b27..0e7467c56 100644 --- a/backend/src/app/schemas/students.py +++ b/backend/src/app/schemas/students.py @@ -74,8 +74,7 @@ class CommonQueryParams: @dataclass class EmailsSearchQueryParams: """search query paramaters for email""" - first_name: str = "" - last_name: str = "" + name: str = "" email_status: list[EmailStatusEnum] = Query([]) page: int = 0 diff --git a/backend/src/database/crud/students.py b/backend/src/database/crud/students.py index 956039d7f..17d61aa43 100644 --- a/backend/src/database/crud/students.py +++ b/backend/src/database/crud/students.py @@ -67,8 +67,7 @@ def get_last_emails_of_students(db: Session, edition: Edition, commons: EmailsSe last_emails = db.query(DecisionEmail.email_id, func.max(DecisionEmail.date))\ .join(Student)\ .where(Student.edition == edition)\ - .where(Student.first_name.contains(commons.first_name))\ - .where(Student.last_name.contains(commons.last_name))\ + .where((Student.first_name + ' ' + Student.last_name).contains(commons.name))\ .group_by(DecisionEmail.student_id).subquery() emails = db.query(DecisionEmail).join( diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index 76a5e8d46..2299b7853 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -394,7 +394,7 @@ def test_get_last_emails_of_students_first_name(database_with_data: Session): create_email(database_with_data, student1, EmailStatusEnum.CONTRACT_CONFIRMED) emails: list[DecisionEmail] = get_last_emails_of_students( - database_with_data, edition, EmailsSearchQueryParams(first_name="Jos", email_status=[])) + database_with_data, edition, EmailsSearchQueryParams(name="Jos", email_status=[])) assert len(emails) == 1 assert emails[0].student_id == 1 @@ -413,7 +413,7 @@ def test_get_last_emails_of_students_last_name(database_with_data: Session): create_email(database_with_data, student1, EmailStatusEnum.CONTRACT_CONFIRMED) emails: list[DecisionEmail] = get_last_emails_of_students( - database_with_data, edition, EmailsSearchQueryParams(last_name="Vermeulen", email_status=[])) + database_with_data, edition, EmailsSearchQueryParams(name="Vermeulen", email_status=[])) assert len(emails) == 1 assert emails[0].student_id == 1 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 36511300a..9b767cbe5 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 @@ -536,8 +536,10 @@ def test_emails_filter_first_name(database_with_data: Session, auth_client: Auth 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/?first_name=Jos") + "/editions/ed2022/students/emails/?name=Jos") assert len(response.json()["studentEmails"]) == 1 assert response.json()["studentEmails"][0]["student"]["firstName"] == "Jos" @@ -547,8 +549,10 @@ def test_emails_filter_last_name(database_with_data: Session, auth_client: AuthC 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/?last_name=Vermeulen") + "/editions/ed2022/students/emails/?name=Vermeulen") assert len(response.json()["studentEmails"]) == 1 assert response.json()[ "studentEmails"][0]["student"]["lastName"] == "Vermeulen" From a152871512c2e2dd9fb0fd0b86fccbf9df3bde5a Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Tue, 26 Apr 2022 20:34:22 +0200 Subject: [PATCH 502/536] Support for long items changes to adding skills --- .../AddedItems/AddedItems.tsx | 4 +- .../AddedSkills/AddedSkills.tsx | 88 +++++++++++++------ .../AddedSkills/styles.ts | 10 ++- .../InputFields/Coach/Coach.tsx | 2 +- .../InputFields/Partner/Partner.tsx | 2 +- .../InputFields/Skill/Skill.tsx | 4 +- .../CreateProjectComponents/styles.ts | 8 +- frontend/src/data/interfaces/projects.ts | 4 +- .../projectViews/CreateProjectPage/styles.ts | 4 +- 9 files changed, 84 insertions(+), 42 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/AddedItems.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/AddedItems.tsx index 9dbc6f551..2a0d6a434 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/AddedItems.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/AddedItems.tsx @@ -1,5 +1,5 @@ import { TiDeleteOutline } from "react-icons/ti"; -import { AddedItem, RemoveButton } from "../styles"; +import { AddedItem, ItemName, RemoveButton } from "../styles"; export default function AddedItems({ items, @@ -12,7 +12,7 @@ export default function AddedItems({
                                    {items.map((element, _index) => ( - {element} + {element} { const newItems = [...items]; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills.tsx index d29baa8d4..1194c5c87 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/AddedSkills.tsx @@ -1,8 +1,23 @@ import { SkillProject } from "../../../../data/interfaces/projects"; import { Input } from "../styles"; -import { AmountInput, SkillContainer, DescriptionContainer, Delete, TopContainer } from "./styles"; +import { + AmountInput, + SkillContainer, + DescriptionContainer, + Delete, + TopContainer, + SkillName, +} from "./styles"; import { TiDeleteOutline } from "react-icons/ti"; +import React from "react"; +/** + * + * @param skills the state of the added skills + * @param setSkills used to update the added skills and there attributes + + * @returns a react component of all the added skills + */ export default function AddedSkills({ skills, setSkills, @@ -10,54 +25,69 @@ export default function AddedSkills({ skills: SkillProject[]; setSkills: (skills: SkillProject[]) => void; }) { + /** + * This function is called when an input field is changed. + * @param event a react event + * @param index the index of the skill to change + * @param amount whether to update the amount (true) or to update the description (false) + */ + function updateSkills( + event: React.ChangeEvent, + index: number, + amount: boolean + ) { + const newList = skills.map((item, otherIndex) => { + if (index === otherIndex) { + if (amount && !isNaN(event.target.valueAsNumber)) { + return { + ...item, + amount: event.target.valueAsNumber, + }; + } + return { + ...item, + description: event.target.value, + }; + } + return item; + }); + setSkills(newList); + } + return (
                                    - {skills.map((skill, _index) => ( - + {skills.map((skill, index) => ( + - {skill.skill} + {skill.skill} + { - const newList = skills.map((item, otherIndex) => { - if (_index === otherIndex) { - const updatedItem = { - ...item, - amount: event.target.valueAsNumber, - }; - return updatedItem; - } - return item; - }); - - setSkills(newList); + updateSkills(event, index, true); }} /> - + { + const newSkills = [...skills]; + newSkills.splice(index, 1); + setSkills(newSkills); + }} + > + { - const newList = skills.map((item, otherIndex) => { - if (_index === otherIndex) { - const updatedItem = { - ...item, - description: event.target.value, - }; - return updatedItem; - } - return item; - }); - - setSkills(newList); + updateSkills(event, index, false); }} /> diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/styles.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/styles.ts index 6e1ed21be..54fc29ae1 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/styles.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/styles.ts @@ -5,7 +5,8 @@ export const SkillContainer = styled.div` margin-top: 10px; background-color: #1a1a36; padding: 5px 10px; - width: fit-content; + width: min-content; + max-width: 75%; `; export const TopContainer = styled.div` @@ -14,6 +15,11 @@ export const TopContainer = styled.div` justify-content: space-between; `; +export const SkillName = styled.div` + overflow-x: scroll; + text-overflow: ellipsis; +`; + export const Delete = styled.button` background-color: #f14a3b; border: 0; @@ -26,6 +32,7 @@ export const Delete = styled.button` export const DescriptionContainer = styled.div` margin-bottom: 10px; + width: fit-content; `; export const AmountInput = styled.input` @@ -35,6 +42,5 @@ export const AmountInput = styled.input` color: white; border: none; border-radius: 5px; - width: 100px; `; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx index 544330504..a71529450 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx @@ -14,7 +14,7 @@ export default function Coach({ setCoaches: (coaches: string[]) => void; }) { const [showAlert, setShowAlert] = useState(false); - const availableCoaches = ["coach1", "coach2", "admin1", "admin2"]; // TODO get users from API call + const availableCoaches = ["coach1", "coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2", "admin1", "admin2"]; // TODO get users from API call return (
                                    diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx index 29cdc1d97..aa0b5b557 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx @@ -30,7 +30,7 @@ export default function Partner({ { - if (!partners.includes(partner)) { + if (!partners.includes(partner) && partner.length > 0) { const newPartners = [...partners]; newPartners.push(partner); setPartners(newPartners); diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx index 5d4124b47..588e846d4 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Skill/Skill.tsx @@ -34,8 +34,8 @@ export default function Skill({ const newSkills = [...skills]; const newSkill: SkillProject = { skill: skill, - description: undefined, - amount: undefined, + description: "", + amount: 1, }; newSkills.push(newSkill); setSkills(newSkills); diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts index 8c73747c4..4a972830f 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts @@ -29,12 +29,18 @@ export const RemoveButton = styled.button` align-items: center; `; +export const ItemName = styled.div` + overflow-x: scroll; + text-overflow: ellipsis; +`; + export const AddedItem = styled.div` margin: 5px; margin-left: 0; padding: 5px; background-color: #1a1a36; - max-width: fit-content; + width: fit-content; + max-width: 75%; border-radius: 5px; display: flex; `; diff --git a/frontend/src/data/interfaces/projects.ts b/frontend/src/data/interfaces/projects.ts index 18aeb1730..1861fb993 100644 --- a/frontend/src/data/interfaces/projects.ts +++ b/frontend/src/data/interfaces/projects.ts @@ -61,10 +61,10 @@ export interface SkillProject { skill: string; /** More info about this skill in a specific project */ - description: string | undefined; + description: string; /** Number of positions of this skill in a project */ - amount: number | undefined; + amount: number; } /** diff --git a/frontend/src/views/projectViews/CreateProjectPage/styles.ts b/frontend/src/views/projectViews/CreateProjectPage/styles.ts index d924e3d74..719aed9f1 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/styles.ts +++ b/frontend/src/views/projectViews/CreateProjectPage/styles.ts @@ -38,11 +38,11 @@ export const CreateButton = styled.button` background-color: #44dba4; color: white; border: none; - margin-top: 10px; + margin-top: 30px; border-radius: 5px; `; export const Label = styled.h5` - margin-top: 20px; + margin-top: 30px; margin-bottom: 0px; ` From ac4f119d48f7b1512dea8f4e2f561584cfd16c62 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Wed, 27 Apr 2022 09:16:52 +0200 Subject: [PATCH 503/536] Move AuthType to Data --- .../src/components/GeneralComponents/AuthTypeIcon.tsx | 2 +- frontend/src/data/enums/authType.ts | 8 ++++++++ frontend/src/data/enums/index.ts | 1 + frontend/src/utils/api/users/users.ts | 10 +--------- 4 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 frontend/src/data/enums/authType.ts diff --git a/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx b/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx index 7bf4af1c7..9f38b8d1a 100644 --- a/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx +++ b/frontend/src/components/GeneralComponents/AuthTypeIcon.tsx @@ -1,6 +1,6 @@ import { HiOutlineMail } from "react-icons/hi"; import { AiFillGithub, AiFillGoogleCircle, AiOutlineQuestionCircle } from "react-icons/ai"; -import { AuthType } from "../../utils/api/users/users"; +import { AuthType } from "../../data/enums"; /** * An icon representing the type of authentication diff --git a/frontend/src/data/enums/authType.ts b/frontend/src/data/enums/authType.ts new file mode 100644 index 000000000..53e5ffd73 --- /dev/null +++ b/frontend/src/data/enums/authType.ts @@ -0,0 +1,8 @@ +/** + * Enum for the different types of authentication. + */ +export enum AuthType { + Email = "email", + GitHub = "github", + Google = "google", +} diff --git a/frontend/src/data/enums/index.ts b/frontend/src/data/enums/index.ts index a7477b60a..e9e2bc5f0 100644 --- a/frontend/src/data/enums/index.ts +++ b/frontend/src/data/enums/index.ts @@ -1,3 +1,4 @@ export { LocalStorageKey } from "./local-storage"; export { SessionStorageKey } from "./session-storage"; export { Role } from "./role"; +export { AuthType } from "./authType"; diff --git a/frontend/src/utils/api/users/users.ts b/frontend/src/utils/api/users/users.ts index 339399786..bf790f2c6 100644 --- a/frontend/src/utils/api/users/users.ts +++ b/frontend/src/utils/api/users/users.ts @@ -1,13 +1,5 @@ import { axiosInstance } from "../api"; - -/** - * Enum for the different types of authentication. - */ -export enum AuthType { - Email = "email", - GitHub = "github", - Google = "google", -} +import { AuthType } from "../../../data/enums"; /** * Interface of a user. From 11e1eaaff6efc7964c90d72b88d3112977dc52e7 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Wed, 27 Apr 2022 12:50:34 +0200 Subject: [PATCH 504/536] add tests --- .../test_database/test_crud/test_students.py | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/backend/tests/test_database/test_crud/test_students.py b/backend/tests/test_database/test_crud/test_students.py index 2299b7853..db9001edd 100644 --- a/backend/tests/test_database/test_crud/test_students.py +++ b/backend/tests/test_database/test_crud/test_students.py @@ -136,6 +136,15 @@ def test_search_students_on_last_name(database_with_data: Session): assert len(students) == 1 +def test_search_students_on_between_first_and_last_name(database_with_data: Session): + """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")) + assert len(students) == 1 + + def test_search_students_alumni(database_with_data: Session): """tests search on alumni""" edition: Edition = database_with_data.query( @@ -420,6 +429,25 @@ 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): + """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( + database_with_data, edition, EmailsSearchQueryParams(name="os V", email_status=[])) + + assert len(emails) == 1 + assert emails[0].student_id == 1 + assert emails[0].decision == EmailStatusEnum.CONTRACT_CONFIRMED + + def test_get_last_emails_of_students_filter_mutliple_status(database_with_data: Session): """tests get all emails where last emails is applied""" student1: Student = get_student_by_id(database_with_data, 1) @@ -430,8 +458,7 @@ def test_get_last_emails_of_students_filter_mutliple_status(database_with_data: create_email(database_with_data, student1, EmailStatusEnum.CONTRACT_CONFIRMED) emails: list[DecisionEmail] = get_last_emails_of_students( - database_with_data, edition, EmailsSearchQueryParams(email_status= - [ + database_with_data, edition, EmailsSearchQueryParams(email_status=[ EmailStatusEnum.APPLIED, EmailStatusEnum.CONTRACT_CONFIRMED ])) From 230bcd6c8b41b96e4b01617387d358995a1a7074 Mon Sep 17 00:00:00 2001 From: Ward Meersman Date: Wed, 27 Apr 2022 12:54:03 +0200 Subject: [PATCH 505/536] added tests --- .../test_students/test_students.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) 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 9b767cbe5..59ee28291 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 @@ -262,6 +262,16 @@ def test_get_last_name_students_pagination(database_with_data: Session, auth_cli 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): + """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 + + def test_get_alumni_students(database_with_data: Session, auth_client: AuthClient): """tests get students based on query paramer alumni""" edition: Edition = database_with_data.query(Edition).all()[0] @@ -558,6 +568,22 @@ def test_emails_filter_last_name(database_with_data: Session, auth_client: AuthC "studentEmails"][0]["student"]["lastName"] == "Vermeulen" +def test_emails_filter_between_first_and_last_name(database_with_data: Session, 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") + assert len(response.json()["studentEmails"]) == 1 + assert response.json()[ + "studentEmails"][0]["student"]["firstName"] == "Jos" + assert response.json()[ + "studentEmails"][0]["student"]["lastName"] == "Vermeulen" + + def test_emails_filter_emailstatus(database_with_data: Session, auth_client: AuthClient): """test to get all email status, and you only filter on the email send""" auth_client.admin() From 08aa5f5adcef648a2e45615b0faa9f28a80db432 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Wed, 27 Apr 2022 13:43:53 +0200 Subject: [PATCH 506/536] fix invite_links --- backend/src/database/crud/invites.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/database/crud/invites.py b/backend/src/database/crud/invites.py index 988a52ba4..cb7dfb777 100644 --- a/backend/src/database/crud/invites.py +++ b/backend/src/database/crud/invites.py @@ -40,7 +40,7 @@ def get_pending_invites_for_edition_page(db: Session, edition: Edition, page: in def get_optional_invite_link_by_edition_and_email(db: Session, edition: Edition, email: str) -> InviteLink | None: """Return an optional invite link by edition and target_email""" - return db.query(InviteLink).where(InviteLink.edition == edition and InviteLink.target_email == email).one_or_none() + return db.query(InviteLink).where(InviteLink.edition == edition).where(InviteLink.target_email == email).one_or_none() def get_invite_link_by_uuid(db: Session, invite_uuid: str | UUID) -> InviteLink: From 017cfe7b02f425f6c6c2cf27c1ce1f2671abab93 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Wed, 27 Apr 2022 13:48:06 +0200 Subject: [PATCH 507/536] fix linting --- backend/src/database/crud/invites.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/database/crud/invites.py b/backend/src/database/crud/invites.py index cb7dfb777..9a794832b 100644 --- a/backend/src/database/crud/invites.py +++ b/backend/src/database/crud/invites.py @@ -40,7 +40,11 @@ def get_pending_invites_for_edition_page(db: Session, edition: Edition, page: in def get_optional_invite_link_by_edition_and_email(db: Session, edition: Edition, email: str) -> InviteLink | None: """Return an optional invite link by edition and target_email""" - return db.query(InviteLink).where(InviteLink.edition == edition).where(InviteLink.target_email == email).one_or_none() + return db\ + .query(InviteLink)\ + .where(InviteLink.edition == edition)\ + .where(InviteLink.target_email == email)\ + .one_or_none() def get_invite_link_by_uuid(db: Session, invite_uuid: str | UUID) -> InviteLink: From 29e4afc2163329637b742fe23a47d9b2c3fc53c2 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 27 Apr 2022 14:13:20 +0200 Subject: [PATCH 508/536] change delete icon --- .../components/ProjectsComponents/ProjectCard/ProjectCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index a24c68260..6dd9aa798 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -14,7 +14,7 @@ import { } from "./styles"; import { BsPersonFill } from "react-icons/bs"; -import { TiDeleteOutline } from "react-icons/ti"; +import { HiOutlineTrash } from "react-icons/hi"; import { useState } from "react"; @@ -66,7 +66,7 @@ export default function ProjectCard({ - + Date: Wed, 27 Apr 2022 19:11:09 +0200 Subject: [PATCH 509/536] update user_manual --- files/user_manual.pdf | Bin 79234 -> 121202 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/files/user_manual.pdf b/files/user_manual.pdf index e62e2df7324f7e0329a6a9fb185dc7ce7682f1f9..daa8023630d294f5ecd3f5734f3c900032a43a4d 100644 GIT binary patch literal 121202 zcma&NQ;;uA@GSTn+qP}nw$9kLZS#z6+x8jTwr!jH-QB%U|Bcw&4;9tX5#3J}nORv) zrYI^-$3)KtO*XeQyavt2LC8pGZ)63{%L~mQYieiiVnN8n&df&me=leTaZ4K)Qzt?O zaT`MyQ&Cf6dlOS=etu|Y7bjCgTWF7sVRf0fO?JedQ}s7BIz$@c3Um7`LQCalL4-|} z#o(2L4Ple{_#@Ew*Y7ZvWRp4!DlaN@4FriJhm$OdUH)#bM`+bVmaG# zH82Z!@+k?I7MkMwl|`W!h>UbyXRilQj}}>d;IE&{x~=wOR_X4h#q5rmsy3u1u*N=Z za*M291|l(rbh6lN*Q>4fX^2gy`aJ;AMPNi@2pBn;I;z<03qxe8oAV2R$O|Ooh+AXB z8z{W0V~Xic!8kbhh^b~Hjlc7!@8<1#mf~IqMp8TVV@hc0hKU$bm2y+yt3py7xC$^K z@W<3&5xJ_mr&(t6`cJnZvsU*V{^(Y3Bl&F(20Xtfz2?@moU+BeR6e=2Vwqd=x!Sa( zXyrSc9NSpi-hbzvw6jJvFs-l?@XtqKn-*meYy=j9o1@sA&ei`;YG?#yo2yLKPhZsG z-JQMwn#5mOZI_NOhpy};R!JpeZKi7L`ckcL=-SQkd$=q3JKiIca>`$?}r z3-7Z$a;}A&q4zlsT++8Q+nVIQdgQUO!@nGV`EfV!8W5)R`udFA7|C!lBn;iIVNqc? zW3RO#;^54anNfF%)fUmmm zW()%YmnX{38QUL!`t-375hZx=AeGS^eb-X!Ay#a;;JEHnyQD=d44zYd5~7*^O{F-g z(H%i^?rxm-B?wXF44bAj9lefr{?m+gt&RfLN^YQ{*{M>@Xe5o~Z`N e0tJKha`X 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 z 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 From 9db4af2b1e4427fb8fc846b593845e65e73e604c Mon Sep 17 00:00:00 2001 From: stijndcl Date: Wed, 27 Apr 2022 19:36:22 +0200 Subject: [PATCH 510/536] Update footer links --- frontend/src/components/Footer/FooterLinks.tsx | 17 ++++++++++++++--- frontend/src/components/Footer/styles.ts | 1 + 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Footer/FooterLinks.tsx b/frontend/src/components/Footer/FooterLinks.tsx index 6f2f1e97f..751e84f4c 100644 --- a/frontend/src/components/Footer/FooterLinks.tsx +++ b/frontend/src/components/Footer/FooterLinks.tsx @@ -4,11 +4,22 @@ import { BASE_URL } from "../../settings"; export default function FooterLinks() { return ( - + -

                                    API

                                    - Documentation +

                                    Documentation

                                    + Backend API +
                                    + {/* This link is always production because we don't host the docs locally */} + Frontend +
                                    + + User Manual + diff --git a/frontend/src/components/Footer/styles.ts b/frontend/src/components/Footer/styles.ts index b8a9bee8c..efade287b 100644 --- a/frontend/src/components/Footer/styles.ts +++ b/frontend/src/components/Footer/styles.ts @@ -12,6 +12,7 @@ export const FooterTitle = styled.h3` export const FooterLink = styled.a` color: white; transition: 200ms ease-out; + text-decoration: none; &:hover { color: var(--osoc_green); From cfcb7e68392e5636d2291f510dfbbf0d2733abb1 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Wed, 27 Apr 2022 19:54:45 +0200 Subject: [PATCH 511/536] Add students email link to navbar --- frontend/src/components/Navbar/Navbar.tsx | 9 +++---- .../components/Navbar/StudentsDropdown.tsx | 27 +++++++++++++++++++ .../src/components/Navbar/UsersDropdown.tsx | 6 ++--- 3 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 frontend/src/components/Navbar/StudentsDropdown.tsx diff --git a/frontend/src/components/Navbar/Navbar.tsx b/frontend/src/components/Navbar/Navbar.tsx index e02ed3f68..7d4a1dac9 100644 --- a/frontend/src/components/Navbar/Navbar.tsx +++ b/frontend/src/components/Navbar/Navbar.tsx @@ -10,13 +10,14 @@ import UsersDropdown from "./UsersDropdown"; import NavbarBase from "./NavbarBase"; import { LinkContainer } from "react-router-bootstrap"; import EditionNavLink from "./EditionNavLink"; +import StudentsDropdown from "./StudentsDropdown"; /** * Navbar component displayed at the top of the screen. * If the user is not signed in, this is hidden automatically. */ export default function Navbar() { - const { isLoggedIn, editions } = useAuth(); + const { isLoggedIn, editions, role } = useAuth(); /** * Important: DO NOT MOVE THIS LINE UNDERNEATH THE RETURN! * Placing an early return above a React hook (in this case, useLocation) causes @@ -68,11 +69,9 @@ export default function Navbar() { Projects - - Students - - + + diff --git a/frontend/src/components/Navbar/StudentsDropdown.tsx b/frontend/src/components/Navbar/StudentsDropdown.tsx new file mode 100644 index 000000000..355e1c11b --- /dev/null +++ b/frontend/src/components/Navbar/StudentsDropdown.tsx @@ -0,0 +1,27 @@ +import NavDropdown from "react-bootstrap/NavDropdown"; +import { LinkContainer } from "react-router-bootstrap"; +import { StyledDropdownItem } from "./styles"; + +interface Props { + isLoggedIn: boolean; + currentEdition: string; +} + +/** + * Dropdown in the [[Navbar]] that allows navigation to the [[StudentsPage]] and [[MailOverviewPage]]. + * @constructor + */ +export default function StudentsDropdown(props: Props) { + if (!props.isLoggedIn) return null; + + return ( + + + Students + + + Email History + + + ); +} diff --git a/frontend/src/components/Navbar/UsersDropdown.tsx b/frontend/src/components/Navbar/UsersDropdown.tsx index a2a543702..b36f08826 100644 --- a/frontend/src/components/Navbar/UsersDropdown.tsx +++ b/frontend/src/components/Navbar/UsersDropdown.tsx @@ -1,4 +1,3 @@ -import { useAuth } from "../../contexts"; import NavDropdown from "react-bootstrap/NavDropdown"; import { StyledDropdownItem } from "./styles"; import { Role } from "../../data/enums"; @@ -7,15 +6,14 @@ import EditionNavLink from "./EditionNavLink"; interface Props { currentEdition: string; + role: Role | null; } /** * NavDropdown that links to the [[AdminsPage]] and [[UsersPage]]. * This component is only rendered for admins. */ -export default function UsersDropdown({ currentEdition }: Props) { - const { role } = useAuth(); - +export default function UsersDropdown({ currentEdition, role }: Props) { // Only admins can see the dropdown because coaches can't // access these pages anyway if (role !== Role.ADMIN) { From d4c7842083f7ed7b9a4f0ccc0c8e7cbf9adcbd64 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 27 Apr 2022 20:15:57 +0200 Subject: [PATCH 512/536] implement requested changes --- frontend/package.json | 1 - frontend/src/utils/api/projects.ts | 6 ++--- .../ProjectsPage/ProjectsPage.tsx | 24 ++++++++++--------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 8046f9e82..99d334d97 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,6 @@ "@fortawesome/fontawesome-svg-core": "^1.3.0", "@fortawesome/free-solid-svg-icons": "^6.0.0", "@fortawesome/react-fontawesome": "^0.1.17", - "@types/react-infinite-scroller": "^1.2.3", "axios": "^0.26.1", "bootstrap": "5.1.3", "buffer": "^6.0.3", diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index c5abd5eaa..b73a2f10b 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { Projects, Project } from "../../data/interfaces/projects"; import { axiosInstance } from "./api"; -export async function getProjects(edition: string, page: number) { +export async function getProjects(edition: string, page: number): Promise { try { const response = await axiosInstance.get( "/editions/" + edition + "/projects/?page=" + page.toString() @@ -18,7 +18,7 @@ export async function getProjects(edition: string, page: number) { } } -export async function getProject(edition: string, projectId: number) { +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; @@ -32,7 +32,7 @@ export async function getProject(edition: string, projectId: number) { } } -export async function deleteProject(edition: string, projectId: number) { +export async function deleteProject(edition: string, projectId: number): Promise { try { await axiosInstance.delete("/editions/" + edition + "/projects/" + projectId); return true; diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index 41e6f0613..d6fa89a54 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -140,17 +140,19 @@ export default function ProjectPage() { - - { - if (moreProjectsAvailable) { - callProjects(page); - } - }} - > - Load more projects - - + {moreProjectsAvailable && ( + + { + if (moreProjectsAvailable) { + callProjects(page); + } + }} + > + Load more projects + + + )}
                                    ); } From a965498fba35d063c47b95c9a291a0d702b1b202 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 27 Apr 2022 20:45:04 +0200 Subject: [PATCH 513/536] use filtering from backend --- frontend/src/Router.tsx | 11 ++-- frontend/src/utils/api/projects.ts | 16 +++++- .../ProjectsPage/ProjectsPage.tsx | 55 +++++-------------- 3 files changed, 34 insertions(+), 48 deletions(-) diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 318690ac2..703e7e015 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -72,13 +72,12 @@ export default function Router() { } /> {/* project page */} - } /> + + } + /> - {/* project page */} - } - /> {/* Students routes */} } /> diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index b73a2f10b..e83f60e9b 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -2,10 +2,22 @@ import axios from "axios"; import { Projects, Project } from "../../data/interfaces/projects"; import { axiosInstance } from "./api"; -export async function getProjects(edition: string, page: number): Promise { +export async function getProjects( + edition: string, + name: string, + ownProjects: boolean, + page: number +): Promise { try { const response = await axiosInstance.get( - "/editions/" + edition + "/projects/?page=" + page.toString() + "/editions/" + + edition + + "/projects/?name=" + + name + + "&coach=" + + ownProjects.toString() + + "&page=" + + page.toString() ); const projects = response.data as Projects; return projects; diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index d6fa89a54..6cbf21dc0 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -11,7 +11,6 @@ import { LoadMoreContainer, LoadMoreButton, } from "./styles"; -import { useAuth } from "../../../contexts/auth-context"; import { Project } from "../../../data/interfaces"; import { useParams } from "react-router-dom"; import InfiniteScroll from "react-infinite-scroller"; @@ -20,55 +19,27 @@ import InfiniteScroll from "react-infinite-scroller"; * You can filter on your own projects or filter on project name. */ export default function ProjectPage() { - const [projectsAPI, setProjectsAPI] = useState([]); + const [projects, setProjects] = useState([]); const [gotProjects, setGotProjects] = useState(false); const [loading, setLoading] = useState(false); const [moreProjectsAvailable, setMoreProjectsAvailable] = useState(true); // Endpoint has more coaches available - // To filter projects we need to keep a separate list to avoid calling the API every time we change te filters. - const [projects, setProjects] = useState([]); - // Keep track of the set filters const [searchString, setSearchString] = useState(""); const [ownProjects, setOwnProjects] = useState(false); const [page, setPage] = useState(0); - const { userId } = useAuth(); - const params = useParams(); const editionId = params.editionId!; - /** - * Uses to filter the results based onto search string and own projects - */ - useEffect(() => { - const results: Project[] = []; - projectsAPI.forEach(project => { - let filterOut = false; - if (ownProjects) { - // If the user doesn't coach this project it will be filtered out. - filterOut = !project.coaches.some(coach => { - return coach.userId === userId; - }); - } - if ( - project.name.toLocaleLowerCase().includes(searchString.toLocaleLowerCase()) && - !filterOut - ) { - results.push(project); - } - }); - setProjects(results); - }, [projectsAPI, ownProjects, searchString, userId]); - /** * Used to fetch the projects */ async function callProjects(newPage: number) { if (loading) return; setLoading(true); - const response = await getProjects(editionId, newPage); + const response = await getProjects(editionId, searchString, ownProjects, newPage); setGotProjects(true); if (response) { @@ -76,13 +47,19 @@ export default function ProjectPage() { setMoreProjectsAvailable(false); } else { setPage(page + 1); - setProjectsAPI(projectsAPI.concat(response.projects)); setProjects(projects.concat(response.projects)); } } setLoading(false); } + async function refreshProjects() { + setProjects([]); + setPage(0); + setMoreProjectsAvailable(true); + setGotProjects(false); + } + useEffect(() => { if (moreProjectsAvailable && !gotProjects) { callProjects(0); @@ -96,8 +73,11 @@ export default function ProjectPage() { value={searchString} onChange={e => setSearchString(e.target.value)} placeholder="project name" + onKeyDown={e => { + if (e.key === "Enter") refreshProjects(); + }} /> - Search + Search Create Project
                                    { setOwnProjects(!ownProjects); + refreshProjects(); }} /> @@ -124,13 +105,7 @@ export default function ProjectPage() { {projects.map((project, _index) => ( { - setProjectsAPI([]); - setProjects([]); - setGotProjects(false); - setPage(0); - setMoreProjectsAvailable(true); - }} + refreshProjects={refreshProjects} key={_index} /> ))} From 9f8520ff2b5b4c6e05e2eeaf119737a5a207ac94 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 27 Apr 2022 21:49:24 +0200 Subject: [PATCH 514/536] updated user manual for projects page --- files/user_manual.md | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/files/user_manual.md b/files/user_manual.md index dd7005945..5baf11504 100644 --- a/files/user_manual.md +++ b/files/user_manual.md @@ -23,7 +23,7 @@ There are different ways to log in, depending on the way in which you have regis ## 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. +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. 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. @@ -40,7 +40,7 @@ At the top left, you can invite someone via an invite link. You can choose betwe #### Requests -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. +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. @@ -66,7 +66,7 @@ Next to the email address, there is a button to remove a user as admin. Once cli - **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. -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. @@ -115,3 +115,23 @@ _**Note**: This dropdown is hidden if you cannot see any editions, as it would b - 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. + +## Projects + +This section contains all actions related to managing projects. + +### Viewing the grid of all 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. + +To get more search results click the **Load more projects** button located at the bottom of the search results + +### 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 + +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 From 4094076dac508351414243b0767f94f815d514bf Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 27 Apr 2022 22:14:40 +0200 Subject: [PATCH 515/536] update import --- frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index a29a200dc..59c4d9fbc 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -14,6 +14,7 @@ import { import { Project } from "../../../data/interfaces"; import { useNavigate, useParams } from "react-router-dom"; import InfiniteScroll from "react-infinite-scroller"; +import { useAuth } from "../../../contexts"; /** * @returns The projects overview page where you can see all the projects. * You can filter on your own projects or filter on project name. @@ -29,7 +30,7 @@ export default function ProjectPage() { const [ownProjects, setOwnProjects] = useState(false); const navigate = useNavigate(); - const { userId, role } = useAuth(); + const { role } = useAuth(); const [page, setPage] = useState(0); const params = useParams(); From 38a6b93a9fd2a2a9cf2dd01d0db52d67bff3d862 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 27 Apr 2022 22:18:03 +0200 Subject: [PATCH 516/536] No to hardcoded editions in links --- .../CreateProjectPage/CreateProjectPage.tsx | 11 +++++++---- .../views/projectViews/ProjectsPage/ProjectsPage.tsx | 4 +++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx index 00d0e9f7d..bbedfae59 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx +++ b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx @@ -1,7 +1,7 @@ import { CreateProjectContainer, CreateButton, Label } from "./styles"; import { createProject } from "../../../utils/api/projects"; import { useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { GoBack } from "../ProjectDetailPage/styles"; import { BiArrowBack } from "react-icons/bi"; import { @@ -33,9 +33,12 @@ export default function CreateProjectPage() { const navigate = useNavigate(); + const params = useParams(); + const editionId = params.editionId!; + return ( - navigate("/editions/2022/projects/")}> + navigate("/editions/" + editionId + "/projects/")}> Cancel @@ -75,7 +78,7 @@ export default function CreateProjectPage() { { const response = await createProject( - "2022", + editionId, name, numberOfStudents!, [], @@ -83,7 +86,7 @@ export default function CreateProjectPage() { coaches ); if (response) { - navigate("/editions/2022/projects/"); + navigate("/editions/" + editionId + "/projects/"); } else alert("Something went wrong :("); }} > diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index 59c4d9fbc..4819ecf74 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -82,7 +82,9 @@ export default function ProjectPage() { /> Search {role === 0 ? ( - navigate("/editions/summerof2022/projects/new")}> + navigate("/editions/" + editionId + "/projects/new")} + > Create Project ) : null} From 60c702eb6925dff4834d77258059570d7563a0d3 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Wed, 27 Apr 2022 22:55:16 +0200 Subject: [PATCH 517/536] use api calls to show available coaches to add to a project --- .../InputFields/Coach/Coach.tsx | 24 +++++++++++++++---- frontend/src/data/interfaces/users.ts | 1 + 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx index a71529450..b5c0c489a 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx @@ -1,5 +1,8 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Alert } from "react-bootstrap"; +import { useParams } from "react-router-dom"; +import { getCoaches } from "../../../../../utils/api/users/coaches"; +import { User } from "../../../../../utils/api/users/users"; import { AddButton, Input, WarningContainer } from "../../styles"; export default function Coach({ @@ -14,25 +17,36 @@ export default function Coach({ setCoaches: (coaches: string[]) => void; }) { const [showAlert, setShowAlert] = useState(false); - const availableCoaches = ["coach1", "coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2coach2", "admin1", "admin2"]; // TODO get users from API call + 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)} + onChange={e => { + setCoach(e.target.value); + }} list="users" placeholder="Coach" /> {availableCoaches.map((availableCoach, _index) => { - return { - if (availableCoaches.some(availableCoach => availableCoach === coach)) { + if (availableCoaches.some(availableCoach => availableCoach.name === coach)) { if (!coaches.includes(coach)) { const newCoaches = [...coaches]; newCoaches.push(coach); diff --git a/frontend/src/data/interfaces/users.ts b/frontend/src/data/interfaces/users.ts index 1c653263d..9092af352 100644 --- a/frontend/src/data/interfaces/users.ts +++ b/frontend/src/data/interfaces/users.ts @@ -9,3 +9,4 @@ export interface User { admin: boolean; editions: string[]; } + From b1fb4cc0c8ac55443b77a7d508c9c98a720751ec Mon Sep 17 00:00:00 2001 From: stijndcl Date: Thu, 28 Apr 2022 08:58:41 +0200 Subject: [PATCH 518/536] Set new edition as current --- frontend/src/views/CreateEditionPage/CreateEditionPage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx b/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx index 7a18c7110..e582a6c13 100644 --- a/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx +++ b/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx @@ -4,6 +4,7 @@ import { createEdition, getSortedEditions } from "../../utils/api/editions"; import { useNavigate } from "react-router-dom"; import { CreateEditionDiv, Error, FormGroup, ButtonDiv } from "./styles"; import { useAuth } from "../../contexts"; +import { setCurrentEdition } from "../../utils/session-storage"; /** * Page to create a new edition. @@ -26,6 +27,7 @@ export default function CreateEditionPage() { if (response === 201) { const allEditions = await getSortedEditions(); setEditions(allEditions); + setCurrentEdition(name); return true; } else if (response === 409) { setNameError("Edition name already exists."); From 8d3c0d4018f2998c0e98658b965804a8e8890ec1 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Thu, 28 Apr 2022 09:02:23 +0200 Subject: [PATCH 519/536] Use response name just in case it ever changes --- frontend/src/utils/api/editions.ts | 9 ++++----- .../src/views/CreateEditionPage/CreateEditionPage.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/frontend/src/utils/api/editions.ts b/frontend/src/utils/api/editions.ts index 98d7bdc16..907bc3dda 100644 --- a/frontend/src/utils/api/editions.ts +++ b/frontend/src/utils/api/editions.ts @@ -1,6 +1,6 @@ import { axiosInstance } from "./api"; import { Edition } from "../../data/interfaces"; -import axios from "axios"; +import axios, { AxiosResponse } from "axios"; interface EditionsResponse { editions: Edition[]; @@ -38,14 +38,13 @@ 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 { +export async function createEdition(name: string, year: number): Promise { const payload: EditionFields = { name: name, year: year }; try { - const response = await axiosInstance.post("/editions/", payload); - return response.status; + return await axiosInstance.post("/editions/", payload); } catch (error) { if (axios.isAxiosError(error) && error.response !== undefined) { - return error.response.status; + return error.response; } else { throw error; } diff --git a/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx b/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx index e582a6c13..bb7abeb53 100644 --- a/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx +++ b/frontend/src/views/CreateEditionPage/CreateEditionPage.tsx @@ -24,14 +24,14 @@ export default function CreateEditionPage() { async function sendEdition(name: string, year: number): Promise { const response = await createEdition(name, year); - if (response === 201) { + if (response.status === 201) { const allEditions = await getSortedEditions(); setEditions(allEditions); - setCurrentEdition(name); + setCurrentEdition(response.data.name); return true; - } else if (response === 409) { + } else if (response.status === 409) { setNameError("Edition name already exists."); - } else if (response === 422) { + } else if (response.status === 422) { setNameError("Invalid edition name."); } else { setError("Something went wrong."); From df1604bf8fec839f74f0ef508d5b9112ae78d9e8 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Thu, 28 Apr 2022 09:41:21 +0200 Subject: [PATCH 520/536] Set overflow to auto --- .../src/components/ProjectsComponents/ProjectCard/styles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts index 12f74f201..d051fba64 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts +++ b/frontend/src/components/ProjectsComponents/ProjectCard/styles.ts @@ -42,7 +42,7 @@ export const ClientContainer = styled.div` export const Clients = styled.div` display: flex; - overflow-x: scroll; + overflow-x: auto; `; export const Client = styled.h5` @@ -59,7 +59,7 @@ export const NumberOfStudents = styled.div` export const CoachesContainer = styled.div` display: flex; margin-top: 20px; - overflow-x: scroll; + overflow-x: auto; `; export const CoachContainer = styled.div` From bb4565004f42458fed54dbb2dc94a71bf037b8b7 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Thu, 28 Apr 2022 12:42:23 +0200 Subject: [PATCH 521/536] Reset confirmation button state upon closing the modal --- .../EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx index e8f98f4da..f472d8b1a 100644 --- a/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx +++ b/frontend/src/components/EditionsPage/DeleteEditionModal/DeleteEditionModal.tsx @@ -26,6 +26,7 @@ export default function DeleteEditionModal(props: Props) { function handleClose() { props.setShow(false); setUnderstandClicked(false); + setDisableConfirm(true); } /** @@ -38,9 +39,6 @@ export default function DeleteEditionModal(props: Props) { // Delete the request const statusCode = await deleteEdition(props.edition.name); - // Hide the modal - props.setShow(false); - if (statusCode === 204) { // Remove the edition as current if (getCurrentEdition() === props.edition.name) { @@ -50,6 +48,9 @@ export default function DeleteEditionModal(props: Props) { // Force-reload the page to re-request all data related to editions // (stored in various places such as auth, ...) window.location.reload(); + + // Hide the modal + props.setShow(false); } } From f7d3834b1ad129b5079ec020bcb5eb66d449cb5d Mon Sep 17 00:00:00 2001 From: Stijn De Clercq <60451863+stijndcl@users.noreply.github.com> Date: Thu, 28 Apr 2022 16:22:47 +0200 Subject: [PATCH 522/536] Update typedoc url --- frontend/src/components/Footer/FooterLinks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Footer/FooterLinks.tsx b/frontend/src/components/Footer/FooterLinks.tsx index 751e84f4c..127bc33a7 100644 --- a/frontend/src/components/Footer/FooterLinks.tsx +++ b/frontend/src/components/Footer/FooterLinks.tsx @@ -11,7 +11,7 @@ export default function FooterLinks() { Backend API
                                    {/* This link is always production because we don't host the docs locally */} - Frontend + Frontend
                                    Date: Thu, 28 Apr 2022 16:30:14 +0200 Subject: [PATCH 523/536] update guide --- files/sysadmin_guide.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/files/sysadmin_guide.md b/files/sysadmin_guide.md index bfb7d952a..41bbdfd1c 100644 --- a/files/sysadmin_guide.md +++ b/files/sysadmin_guide.md @@ -34,7 +34,7 @@ To be able to login via ssh a sysadmin should add your ssh key to the `authorize [frontend] $ echo "" >> /home/frontend/.ssh/authorized_keys ``` -See [Generating SSH Keys](#generating-ssh-keys) for instructions on how to generate an ssh key. +See [Generating SSH Keys](#generating-ssh-keys) for instructions on how to generate an ssh key. #### Installing Node and Yarn @@ -112,7 +112,7 @@ Navigate to the frontend folder in the cloned repository and install the depende Then create the `.env` file. ```shell -[frontend] $ echo "REACT_APP_BASE_URL=https://" > .env +[frontend] $ echo "REACT_APP_BASE_URL=https://" > .env ``` Then build the frontend. @@ -146,7 +146,7 @@ The mariadb version available in the apt repositories (Currently 10.3) is too ou [admin] $ sudo apt install mariadb-server ``` -Once the server is installed **make sure** to run the `mariadb-secure-installation` script. This is **very important!** The database will be insecure if not run. +Once the server is installed **make sure** to run the `mariadb-secure-installation` script. This is **very important!** The database will be insecure if not run. #### Configuring Database @@ -199,8 +199,7 @@ To be able to login via ssh a sysadmin should add your ssh key to the `authorize [backend] $ echo "" >> /home/backend/.ssh/authorized_keys ``` -See [Generating SSH Keys](#generating-ssh-keys) for instructions on how to generate an ssh key. - +See [Generating SSH Keys](#generating-ssh-keys) for instructions on how to generate an ssh key. #### Installing Python @@ -283,7 +282,11 @@ We will be installing dependencies in a virtual environment. You can create and [backend] $ . venv-osoc/bin/activate ``` -To manage dependecies we currently use 2 separate requirements files. Only `requirements.txt` has to be installed. The other one is for development setups. +To manage dependecies we currently use poetry. You can install it using the following command. + +```shell +(venv-osoc) [backend] $ pip3 install poetry +``` Make sure the mariadb libraries are installed. @@ -293,7 +296,7 @@ Make sure the mariadb libraries are installed. ```shell (venv-osoc) [backend] $ cd osoc/backend -(venv-osoc) [backend] $ pip3 install -r requirements.txt +(venv-osoc) [backend] $ python3 -m poetry install --no-dev ``` #### Configuring the application @@ -375,11 +378,11 @@ upstream backend { server { server_name example.com; listen 80 - + location / { root /home/frontend/osoc/frontend/build; index index.html; - + try_files $uri $uri/ =404; } From 48794ff451eab0eda3a8f8935e496d2b02fd4487 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Thu, 28 Apr 2022 16:58:07 +0200 Subject: [PATCH 524/536] fix requested changes --- frontend/src/views/projectViews/ProjectDetailPage/styles.ts | 2 +- .../src/views/projectViews/ProjectsPage/ProjectsPage.tsx | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/projectViews/ProjectDetailPage/styles.ts b/frontend/src/views/projectViews/ProjectDetailPage/styles.ts index ffa7ab91c..914cfd91b 100644 --- a/frontend/src/views/projectViews/ProjectDetailPage/styles.ts +++ b/frontend/src/views/projectViews/ProjectDetailPage/styles.ts @@ -23,7 +23,7 @@ export const ClientContainer = styled.div` display: flex; align-items: center; color: lightgray; - overflow-x: scroll; + overflow-x: auto; `; export const Client = styled.h5` diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index 6cbf21dc0..36292320c 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -14,6 +14,7 @@ import { import { Project } from "../../../data/interfaces"; import { useParams } from "react-router-dom"; import InfiniteScroll from "react-infinite-scroller"; +import { useAuth } from "../../../contexts"; /** * @returns The projects overview page where you can see all the projects. * You can filter on your own projects or filter on project name. @@ -33,6 +34,8 @@ export default function ProjectPage() { const params = useParams(); const editionId = params.editionId!; + const { role } = useAuth(); + /** * Used to fetch the projects */ @@ -78,7 +81,7 @@ export default function ProjectPage() { }} /> Search - Create Project + {role === 0 && Create Project}
                                    Date: Thu, 28 Apr 2022 17:11:02 +0200 Subject: [PATCH 525/536] hide element not allowed to coaches but only to admins --- .../ProjectsComponents/ProjectCard/ProjectCard.tsx | 7 +++++-- .../src/views/projectViews/ProjectsPage/ProjectsPage.tsx | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 6dd9aa798..4340b6d46 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -23,6 +23,7 @@ import { deleteProject } from "../../../utils/api/projects"; import { useNavigate, useParams } from "react-router-dom"; import { Project } from "../../../data/interfaces"; +import { useAuth } from "../../../contexts"; /** * @@ -53,6 +54,8 @@ export default function ProjectCard({ const params = useParams(); const editionId = params.editionId!; + const { role } = useAuth(); + return ( @@ -65,9 +68,9 @@ export default function ProjectCard({ - + {!role && - + } Search - {role === 0 && Create Project} + {!role && Create Project}
                                    Date: Thu, 28 Apr 2022 17:11:45 +0200 Subject: [PATCH 526/536] prettier --- .../ProjectsComponents/ProjectCard/ProjectCard.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index 4340b6d46..d1a0f7e13 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -68,9 +68,11 @@ export default function ProjectCard({ - {!role && - - } + {!role && ( + + + + )} Date: Thu, 28 Apr 2022 15:17:33 +0000 Subject: [PATCH 527/536] Bump ejs from 3.1.6 to 3.1.7 in /frontend Bumps [ejs](https://github.com/mde/ejs) from 3.1.6 to 3.1.7. - [Release notes](https://github.com/mde/ejs/releases) - [Changelog](https://github.com/mde/ejs/blob/main/CHANGELOG.md) - [Commits](https://github.com/mde/ejs/compare/v3.1.6...v3.1.7) --- updated-dependencies: - dependency-name: ejs dependency-type: indirect ... Signed-off-by: dependabot[bot] --- frontend/yarn.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index bf12c500a..1e64c06db 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2641,11 +2641,6 @@ ast-types-flow@^0.0.7: resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= -async@0.9.x: - version "0.9.2" - resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" - integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= - async@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" @@ -2653,6 +2648,11 @@ async@^2.6.2: 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" + 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" @@ -3961,11 +3961,11 @@ ee-first@1.1.1: integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= ejs@^3.1.6: - version "3.1.6" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a" - integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw== + version "3.1.7" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.7.tgz#c544d9c7f715783dd92f0bddcf73a59e6962d006" + integrity sha512-BIar7R6abbUxDA3bfXrO4DSgwo8I+fB5/1zgujl3HLLjwd6+9iOnrT+t3grn2qbk9vOgBubXOFwX2m9axoFaGw== dependencies: - jake "^10.6.1" + jake "^10.8.5" electron-to-chromium@^1.4.84: version "1.4.88" @@ -4631,11 +4631,11 @@ file-loader@^6.2.0: schema-utils "^3.0.0" filelist@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" - integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ== + version "1.0.3" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.3.tgz#448607750376484932f67ef1b9ff07386b036c83" + integrity sha512-LwjCsruLWQULGYKy7TX0OPtrL9kLpojOFKc5VCTxdFTV7w5zbsgqVKfnkKG7Qgjtq50gKfO56hJv88OfcGb70Q== dependencies: - minimatch "^3.0.4" + minimatch "^5.0.1" filesize@^8.0.6: version "8.0.7" @@ -5528,12 +5528,12 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jake@^10.6.1: - version "10.8.4" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.4.tgz#f6a8b7bf90c6306f768aa82bb7b98bf4ca15e84a" - integrity sha512-MtWeTkl1qGsWUtbl/Jsca/8xSoK3x0UmS82sNbjqxxG/de/M/3b1DntdjHgPMC50enlTNwXOCRqPXLLt5cCfZA== +jake@^10.8.5: + version "10.8.5" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" + integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== dependencies: - async "0.9.x" + async "^3.2.3" chalk "^4.0.2" filelist "^1.0.1" minimatch "^3.0.4" From eb6f668b4a98946a9ff73e011d9dd4b3095b7883 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Thu, 28 Apr 2022 17:45:24 +0200 Subject: [PATCH 528/536] Add coaches to a project --- .../AddedCoaches/AddedCoaches.tsx | 30 +++++++++++++++++++ .../AddedCoaches/index.ts | 1 + .../AddedItems/index.ts | 1 - .../AddedPartners.tsx} | 2 +- .../AddedPartners/index.ts | 1 + .../InputFields/Coach/Coach.tsx | 16 ++++++---- .../CreateProjectComponents/index.ts | 3 +- frontend/src/data/interfaces/projects.ts | 4 +-- frontend/src/utils/api/projects.ts | 2 +- .../CreateProjectPage/CreateProjectPage.tsx | 17 +++++++---- 10 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx create mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/index.ts delete mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/index.ts rename frontend/src/components/ProjectsComponents/CreateProjectComponents/{AddedItems/AddedItems.tsx => AddedPartners/AddedPartners.tsx} (95%) create mode 100644 frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedPartners/index.ts diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx new file mode 100644 index 000000000..6417e7964 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/AddedCoaches.tsx @@ -0,0 +1,30 @@ +import { TiDeleteOutline } from "react-icons/ti"; +import { User } from "../../../../utils/api/users/users"; +import { AddedItem, ItemName, RemoveButton } from "../styles"; + +export default function AddedCoaches({ + coaches, + setCoaches, +}: { + coaches: User[]; + setCoaches: (coaches: User[]) => void; +}) { + return ( +
                                    + {coaches.map((element, _index) => ( + + {element.name} + { + const newItems = [...coaches]; + newItems.splice(_index, 1); + setCoaches(newItems); + }} + > + + + + ))} +
                                    + ); +} diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/index.ts new file mode 100644 index 000000000..fe048b5a7 --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedCoaches/index.ts @@ -0,0 +1 @@ +export { default } from "./AddedCoaches"; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/index.ts deleted file mode 100644 index 4c09ff358..000000000 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./AddedItems"; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/AddedItems.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedPartners/AddedPartners.tsx similarity index 95% rename from frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/AddedItems.tsx rename to frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedPartners/AddedPartners.tsx index 2a0d6a434..0cf5b0040 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedItems/AddedItems.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedPartners/AddedPartners.tsx @@ -1,7 +1,7 @@ import { TiDeleteOutline } from "react-icons/ti"; import { AddedItem, ItemName, RemoveButton } from "../styles"; -export default function AddedItems({ +export default function AddedPartners({ items, setItems, }: { diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedPartners/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedPartners/index.ts new file mode 100644 index 000000000..ee4652a7d --- /dev/null +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedPartners/index.ts @@ -0,0 +1 @@ +export { default } from "./AddedPartners"; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx index b5c0c489a..2d653489b 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx @@ -13,8 +13,8 @@ export default function Coach({ }: { coach: string; setCoach: (coach: string) => void; - coaches: string[]; - setCoaches: (coaches: string[]) => void; + coaches: User[]; + setCoaches: (coaches: User[]) => void; }) { const [showAlert, setShowAlert] = useState(false); const [availableCoaches, setAvailableCoaches] = useState([]); @@ -46,10 +46,16 @@ export default function Coach({ { - if (availableCoaches.some(availableCoach => availableCoach.name === coach)) { - if (!coaches.includes(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(coach); + newCoaches.push(coachToAdd); setCoaches(newCoaches); setShowAlert(false); } diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts index 939c037ec..63f743cf3 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/index.ts @@ -5,5 +5,6 @@ export { SkillInput, PartnerInput, } from "./InputFields"; -export { default as AddedItems } from "./AddedItems"; +export { default as AddedPartners } from "./AddedPartners"; +export { default as AddedCoaches } from "./AddedCoaches"; export { default as AddedSkills } from "./AddedSkills"; diff --git a/frontend/src/data/interfaces/projects.ts b/frontend/src/data/interfaces/projects.ts index 1861fb993..6609d8574 100644 --- a/frontend/src/data/interfaces/projects.ts +++ b/frontend/src/data/interfaces/projects.ts @@ -83,8 +83,8 @@ export interface CreateProject { /** The partners that belong to this project */ partners: string[]; - /** The users that will coach this project */ - coaches: string[]; + /** The IDs of the users that will coach this project */ + coaches: number[]; } /** diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index 015ac8ebd..dc49215c9 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -50,7 +50,7 @@ export async function createProject( numberOfStudents: number, skills: string[], partners: string[], - coaches: string[] + coaches: number[] ) { const payload: CreateProject = { name: name, diff --git a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx index bbedfae59..9de88fc6c 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx +++ b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx @@ -10,10 +10,12 @@ import { CoachInput, SkillInput, PartnerInput, - AddedItems, + AddedCoaches, + AddedPartners, AddedSkills, } from "../../../components/ProjectsComponents/CreateProjectComponents"; import { SkillProject } from "../../../data/interfaces/projects"; +import { User } from "../../../utils/api/users/users"; export default function CreateProjectPage() { const [name, setName] = useState(""); @@ -21,7 +23,7 @@ export default function CreateProjectPage() { // States for coaches const [coach, setCoach] = useState(""); - const [coaches, setCoaches] = useState([]); + const [coaches, setCoaches] = useState([]); // States for skills const [skill, setSkill] = useState(""); @@ -60,7 +62,7 @@ export default function CreateProjectPage() { coaches={coaches} setCoaches={setCoaches} /> - + @@ -73,17 +75,22 @@ export default function CreateProjectPage() { partners={partners} setPartners={setPartners} /> - + { + const coachesIds: number[] = []; + coaches.forEach(coachToAdd => { + coachesIds.push(coachToAdd.userId); + }); + const response = await createProject( editionId, name, numberOfStudents!, [], partners, - coaches + coachesIds ); if (response) { navigate("/editions/" + editionId + "/projects/"); From 2773789bbd4969de8f963f2d9b96cb401fa0dffe Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Thu, 28 Apr 2022 18:02:05 +0200 Subject: [PATCH 529/536] Documentation --- frontend/src/data/interfaces/users.ts | 1 - frontend/src/utils/api/projects.ts | 32 ++++++++++++++++++- .../CreateProjectPage/CreateProjectPage.tsx | 12 ++++--- .../projectViews/CreateProjectPage/styles.ts | 2 +- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/frontend/src/data/interfaces/users.ts b/frontend/src/data/interfaces/users.ts index 9092af352..1c653263d 100644 --- a/frontend/src/data/interfaces/users.ts +++ b/frontend/src/data/interfaces/users.ts @@ -9,4 +9,3 @@ export interface User { admin: boolean; editions: string[]; } - diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index dc49215c9..b3efdde13 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -2,6 +2,14 @@ import axios from "axios"; import { Projects, Project, CreateProject } from "../../data/interfaces/projects"; import { axiosInstance } from "./api"; +/** + * API call to get projects (and filter them) + * @param edition The edition name. + * @param name To filter on project name. + * @param ownProjects To filter on your own projects. + * @param page The requested page. + * @returns + */ export async function getProjects( edition: string, name: string, @@ -30,6 +38,12 @@ export async function getProjects( } } +/** + * API call to get a specific project. + * @param edition The edition name. + * @param projectId The ID of the project. + * @returns A Project object when successful. + */ export async function getProject(edition: string, projectId: number): Promise { try { const response = await axiosInstance.get("/editions/" + edition + "/projects/" + projectId); @@ -44,6 +58,16 @@ export async function getProject(edition: string, projectId: number): Promise { const payload: CreateProject = { name: name, number_of_students: numberOfStudents, @@ -74,6 +98,12 @@ export async function createProject( } } +/** + * API call to delete a project. + * @param edition The edition name. + * @param projectId The ID of the project that needs to be deleted. + * @returns true if the deletion was successful or false if it failed. + */ export async function deleteProject(edition: string, projectId: number): Promise { try { await axiosInstance.delete("/editions/" + edition + "/projects/" + projectId); diff --git a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx index 9de88fc6c..49c99ceb5 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx +++ b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx @@ -17,6 +17,10 @@ import { import { SkillProject } from "../../../data/interfaces/projects"; import { User } from "../../../utils/api/users/users"; +/** + * React component of the create project page. + * @returns The create project page. + */ export default function CreateProjectPage() { const [name, setName] = useState(""); const [numberOfStudents, setNumberOfStudents] = useState(0); @@ -79,18 +83,18 @@ export default function CreateProjectPage() { { - const coachesIds: number[] = []; + const coachIds: number[] = []; coaches.forEach(coachToAdd => { - coachesIds.push(coachToAdd.userId); + coachIds.push(coachToAdd.userId); }); const response = await createProject( editionId, name, numberOfStudents!, - [], + [], // Empty skills for now TODO partners, - coachesIds + coachIds ); if (response) { navigate("/editions/" + editionId + "/projects/"); diff --git a/frontend/src/views/projectViews/CreateProjectPage/styles.ts b/frontend/src/views/projectViews/CreateProjectPage/styles.ts index 719aed9f1..6cbd8c1c3 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/styles.ts +++ b/frontend/src/views/projectViews/CreateProjectPage/styles.ts @@ -45,4 +45,4 @@ export const CreateButton = styled.button` export const Label = styled.h5` margin-top: 30px; margin-bottom: 0px; -` +`; From 62f7d8c6dd99baab51ae99179c0c5629f027c271 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Thu, 28 Apr 2022 18:09:04 +0200 Subject: [PATCH 530/536] prettier --- .../CreateProjectComponents/InputFields/Name/Name.tsx | 2 +- .../InputFields/NumberOfStudents/index.ts | 2 +- .../CreateProjectComponents/InputFields/Partner/Partner.tsx | 3 ++- .../CreateProjectComponents/InputFields/index.ts | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Name/Name.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Name/Name.tsx index 0a74208a8..67ad8f7d9 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Name/Name.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Name/Name.tsx @@ -1,4 +1,4 @@ -import { Input } from "../../styles" +import { Input } from "../../styles"; export default function Name({ name, setName }: { name: string; setName: (name: string) => void }) { return ( diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/index.ts index 4eef7d36a..7594e8ecf 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/index.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/index.ts @@ -1 +1 @@ -export {default} from "./NumberOfStudents" \ No newline at end of file +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 aa0b5b557..a6096de81 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Partner/Partner.tsx @@ -34,7 +34,8 @@ export default function Partner({ const newPartners = [...partners]; newPartners.push(partner); setPartners(newPartners); - } setPartner("") + } + setPartner(""); }} > Add partner diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts index 989b56347..b0235977d 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/index.ts @@ -2,4 +2,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"; \ No newline at end of file +export { default as PartnerInput } from "./Partner"; From b5cc2996ae5c2f41012a50b0c0eba56ead07e7b2 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Thu, 28 Apr 2022 18:13:26 +0200 Subject: [PATCH 531/536] clear coach field when add button is clicked QOL --- .../CreateProjectComponents/InputFields/Coach/Coach.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx index 2d653489b..ab5a1ec47 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx @@ -60,6 +60,7 @@ export default function Coach({ setShowAlert(false); } } else setShowAlert(true); + setCoach(""); }} > Add coach From 01ed4a58e074d57c10b421575a3ce9009826b0e5 Mon Sep 17 00:00:00 2001 From: SeppeM8 Date: Thu, 28 Apr 2022 18:47:44 +0200 Subject: [PATCH 532/536] change post & delete in removeAdminAndCoach --- frontend/src/utils/api/users/admins.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/utils/api/users/admins.ts b/frontend/src/utils/api/users/admins.ts index f94c0deef..cc6b8bf12 100644 --- a/frontend/src/utils/api/users/admins.ts +++ b/frontend/src/utils/api/users/admins.ts @@ -38,7 +38,7 @@ export async function removeAdmin(userId: number) { * @param {number} userId The id of the user. */ export async function removeAdminAndCoach(userId: number) { - const response1 = await axiosInstance.patch(`/users/${userId}`, { admin: false }); const response2 = await axiosInstance.delete(`/users/${userId}/editions`); + const response1 = await axiosInstance.patch(`/users/${userId}`, { admin: false }); return response1.status === 204 && response2.status === 204; } From e0b6d8dd2f2ea2341fcbf702a4a2d9a0bd06b9f8 Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Thu, 28 Apr 2022 20:28:02 +0200 Subject: [PATCH 533/536] scroll -> auto --- .../CreateProjectComponents/AddedSkills/styles.ts | 2 +- .../ProjectsComponents/CreateProjectComponents/styles.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/styles.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/styles.ts index 54fc29ae1..9b4a4b069 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/styles.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/AddedSkills/styles.ts @@ -16,7 +16,7 @@ export const TopContainer = styled.div` `; export const SkillName = styled.div` - overflow-x: scroll; + overflow-x: auto; text-overflow: ellipsis; `; diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts b/frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts index 4a972830f..d40fefa38 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/styles.ts @@ -30,7 +30,7 @@ export const RemoveButton = styled.button` `; export const ItemName = styled.div` - overflow-x: scroll; + overflow-x: auto; text-overflow: ellipsis; `; From b737c039ef1c39031a81f88d7f5685e125055f9a Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Thu, 28 Apr 2022 20:30:35 +0200 Subject: [PATCH 534/536] initial value for number of students is 1 --- .../InputFields/NumberOfStudents/NumberOfStudents.tsx | 2 +- .../views/projectViews/CreateProjectPage/CreateProjectPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx index 33279edf6..7586d250b 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/NumberOfStudents/NumberOfStudents.tsx @@ -11,7 +11,7 @@ export default function NumberOfStudents({
                                    { setNumberOfStudents(e.target.valueAsNumber); diff --git a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx index 49c99ceb5..33d800399 100644 --- a/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx +++ b/frontend/src/views/projectViews/CreateProjectPage/CreateProjectPage.tsx @@ -23,7 +23,7 @@ import { User } from "../../../utils/api/users/users"; */ export default function CreateProjectPage() { const [name, setName] = useState(""); - const [numberOfStudents, setNumberOfStudents] = useState(0); + const [numberOfStudents, setNumberOfStudents] = useState(1); // States for coaches const [coach, setCoach] = useState(""); From 190f0c57d0d572cd88f92b0ac0ea1951503e1fc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Apr 2022 18:32:12 +0000 Subject: [PATCH 535/536] Bump async from 2.6.3 to 2.6.4 in /frontend Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4. - [Release notes](https://github.com/caolan/async/releases) - [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md) - [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4) --- updated-dependencies: - dependency-name: async dependency-type: indirect ... Signed-off-by: dependabot[bot] --- frontend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 1e64c06db..b3d4746e3 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2642,9 +2642,9 @@ ast-types-flow@^0.0.7: integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= async@^2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + version "2.6.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== dependencies: lodash "^4.17.14" From 7fef2f0a2e2d931107879f896ee48aef1f2fed0c Mon Sep 17 00:00:00 2001 From: Tiebe Vercoutter Date: Thu, 28 Apr 2022 20:59:02 +0200 Subject: [PATCH 536/536] Use enum for role --- .../components/ProjectsComponents/ProjectCard/ProjectCard.tsx | 3 ++- frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx index d1a0f7e13..ca815f2ee 100644 --- a/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectCard/ProjectCard.tsx @@ -24,6 +24,7 @@ import { useNavigate, useParams } from "react-router-dom"; import { Project } from "../../../data/interfaces"; import { useAuth } from "../../../contexts"; +import { Role } from "../../../data/enums"; /** * @@ -68,7 +69,7 @@ export default function ProjectCard({ - {!role && ( + {role === Role.ADMIN && ( diff --git a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx index 9b803cff3..1fe9daae8 100644 --- a/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/views/projectViews/ProjectsPage/ProjectsPage.tsx @@ -15,6 +15,7 @@ import { Project } from "../../../data/interfaces"; import { useNavigate, useParams } from "react-router-dom"; import InfiniteScroll from "react-infinite-scroller"; import { useAuth } from "../../../contexts"; +import { Role } from "../../../data/enums"; /** * @returns The projects overview page where you can see all the projects. * You can filter on your own projects or filter on project name. @@ -82,7 +83,7 @@ export default function ProjectPage() { }} /> Search - {!role && ( + {role === Role.ADMIN && ( navigate("/editions/" + editionId + "/projects/new")} >