diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 00000000..d40db921 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,115 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = postgresql://selab:2UJd24Z6aUm85ZExEri@sel2-5.ugent.be:2002/selab + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/alembic/README b/backend/alembic/README new file mode 100644 index 00000000..98e4f9c4 --- /dev/null +++ b/backend/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 00000000..de4ae87d --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,97 @@ +from src.user.models import Base as UserBase +from src.subject.models import Base as SubjectBase +from src.project.models import Base as ProjectBase +import os +import sys +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool +from sqlalchemy import MetaData + +from alembic import context +from src.database import Base + +# Calculate the path based on the location of the env.py file +d = os.path.dirname +parent_dir = d(d(os.path.abspath(__file__))) +sys.path.append(parent_dir) + +# Import the Base from each of your model submodules + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +combined_metadata = MetaData() +for base in [ProjectBase, SubjectBase, UserBase]: + for table in base.metadata.tables.values(): + combined_metadata._add_table(table.name, table.schema, table) + +target_metadata = combined_metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 00000000..fbc4b07d --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/alembic/versions/29a17fa183de_enroll_deadline_added.py b/backend/alembic/versions/29a17fa183de_enroll_deadline_added.py new file mode 100644 index 00000000..048c10f8 --- /dev/null +++ b/backend/alembic/versions/29a17fa183de_enroll_deadline_added.py @@ -0,0 +1,30 @@ +"""enroll_deadline_added + +Revision ID: 29a17fa183de +Revises: fc317e930257 +Create Date: 2024-03-11 19:09:35.872572 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '29a17fa183de' +down_revision: Union[str, None] = 'fc317e930257' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('project', sa.Column('enroll_deadline', sa.Date(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('project', 'enroll_deadline') + # ### end Alembic commands ### diff --git a/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py b/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py new file mode 100644 index 00000000..0ebc9588 --- /dev/null +++ b/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py @@ -0,0 +1,159 @@ +"""Allign with new schema + +Revision ID: 2cc559f8470c +Revises: ae4432f8b5b7 +Create Date: 2024-03-11 16:20:20.260148 + +""" +from typing import Sequence, Union + +from alembic import op # type : ignore +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '2cc559f8470c' +down_revision: Union[str, None] = 'ae4432f8b5b7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # Drop tables that are dependent on other tables + op.execute('DROP TABLE IF EXISTS student_subject CASCADE') + op.execute('DROP TABLE IF EXISTS teacher_subject CASCADE') + op.execute('DROP TABLE IF EXISTS student_group CASCADE') + op.execute('DROP TABLE IF EXISTS submission CASCADE') + op.execute('DROP TABLE IF EXISTS file CASCADE') + op.execute('DROP TABLE IF EXISTS website_user CASCADE') + op.execute('DROP TABLE IF EXISTS status CASCADE') + op.execute('DROP TABLE IF EXISTS team CASCADE') + op.execute('DROP TABLE IF EXISTS subject CASCADE') + op.execute('DROP TABLE IF EXISTS project CASCADE') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('file', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('submission_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='file_project_id_fkey', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['submission_id'], ['submission.id'], + name='file_submission_id_fkey', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name='file_pkey') + ) + op.create_table('project', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('project_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('deadline', sa.DATE(), + autoincrement=False, nullable=False), + sa.Column('name', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), server_default=sa.text( + "nextval('project_subject_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('description', sa.TEXT(), + autoincrement=False, nullable=True), + sa.Column('max_team_size', sa.INTEGER(), server_default=sa.text( + '4'), autoincrement=False, nullable=False), + sa.CheckConstraint('deadline >= CURRENT_DATE', + name='deadline_check'), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='project_subject_id_fkey', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name='project_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('teacher_subject', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='teacher_subject_subject_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='teacher_subject_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'subject_id', + name='teacher_subject_pkey') + ) + op.create_table('student_subject', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='student_subject_subject_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='student_subject_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'subject_id', + name='student_subject_pkey') + ) + op.create_table('submission', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('date', postgresql.TIMESTAMP(), server_default=sa.text( + 'CURRENT_TIMESTAMP'), autoincrement=False, nullable=False), + sa.Column('team_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('status_id', sa.BIGINT(), server_default=sa.text( + '1'), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='submission_project_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['status_id'], ['status.id'], + name='submission_status_id_fkey', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['team_id'], ['team.id'], + name='submission_team_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='submission_pkey') + ) + op.create_table('subject', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('subject_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('name', sa.TEXT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='subject_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('team', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('team_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('team_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('score', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.CheckConstraint('score >= 0 AND score <= 20', + name='score_check'), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='team_project_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='team_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('student_group', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('team_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['team_id'], ['team.id'], + name='student_group_team_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='student_group_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'team_id', name='student_group_pkey') + ) + op.create_table('status', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('status_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='status_pkey'), + sa.UniqueConstraint('status_name', name='status_status_name_key') + ) + op.create_table('website_user', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('is_admin', sa.BOOLEAN(), server_default=sa.text( + 'false'), autoincrement=False, nullable=False), + sa.Column('given_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('mail', sa.TEXT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('uid', name='website_user_pkey') + ) + # ### end Alembic commands ### diff --git a/backend/alembic/versions/3756e9987aa1_recreate_tables.py b/backend/alembic/versions/3756e9987aa1_recreate_tables.py new file mode 100644 index 00000000..7353acde --- /dev/null +++ b/backend/alembic/versions/3756e9987aa1_recreate_tables.py @@ -0,0 +1,30 @@ +"""Recreate tables + +Revision ID: 3756e9987aa1 +Revises: 2cc559f8470c +Create Date: 2024-03-11 16:40:14.053191 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '3756e9987aa1' +down_revision: Union[str, None] = '2cc559f8470c' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/backend/alembic/versions/ae4432f8b5b7_initial_migration.py b/backend/alembic/versions/ae4432f8b5b7_initial_migration.py new file mode 100644 index 00000000..dd1ab367 --- /dev/null +++ b/backend/alembic/versions/ae4432f8b5b7_initial_migration.py @@ -0,0 +1,252 @@ +"""Initial migration + +Revision ID: ae4432f8b5b7 +Revises: +Create Date: 2024-03-11 16:00:23.837847 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'ae4432f8b5b7' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('student_subject', cascade='CASCADE') + op.drop_table('studentvak', cascade='CASCADE') + op.drop_table('team', cascade='CASCADE') + op.drop_table('groep', cascade='CASCADE') + op.drop_table('indiening', cascade='CASCADE') + op.drop_table('lesgevervak', cascade='CASCADE') + op.drop_table('teacher_subject', cascade='CASCADE') + op.drop_table('student_group', cascade='CASCADE') + op.drop_table('website_user', cascade='CASCADE') + op.drop_table('bestand', cascade='CASCADE') + op.drop_table('project', cascade='CASCADE') + op.drop_table('submission', cascade='CASCADE') + op.drop_table('status', cascade='CASCADE') + op.drop_table('studentgroep', cascade='CASCADE') + op.drop_table('file', cascade='CASCADE') + op.drop_table('subject', cascade='CASCADE') + op.drop_table('vak', cascade='CASCADE') + op.drop_table('website_user', cascade='CASCADE') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('website_user', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('is_admin', sa.BOOLEAN(), server_default=sa.text( + 'false'), autoincrement=False, nullable=False), + sa.Column('given_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('mail', sa.TEXT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('uid', name='website_user_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('vak', + sa.Column('vak_id', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('naam', sa.TEXT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('vak_id', name='vak_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('subject', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('subject_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('name', sa.TEXT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='subject_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('file', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('submission_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='file_project_id_fkey', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['submission_id'], ['submission.id'], + name='file_submission_id_fkey', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name='file_pkey') + ) + op.create_table('studentgroep', + sa.Column('azureobjectid', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('groep_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['azureobjectid'], [ + 'websiteuser.azureobjectid'], name='studentgroep_azureobjectid_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['groep_id'], ['groep.groep_id'], + name='studentgroep_groep_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint( + 'azureobjectid', 'groep_id', name='studentgroep_pkey') + ) + op.create_table('status', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('status_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('status_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='status_pkey'), + sa.UniqueConstraint('status_name', name='status_status_name_key'), + postgresql_ignore_search_path=False + ) + op.create_table('submission', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('date', postgresql.TIMESTAMP(), server_default=sa.text( + 'CURRENT_TIMESTAMP'), autoincrement=False, nullable=False), + sa.Column('team_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('status_id', sa.BIGINT(), server_default=sa.text( + '1'), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='submission_project_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['status_id'], ['status.id'], + name='submission_status_id_fkey', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['team_id'], ['team.id'], + name='submission_team_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='submission_pkey') + ) + op.create_table('project', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('project_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('deadline', sa.DATE(), + autoincrement=False, nullable=False), + sa.Column('name', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), server_default=sa.text( + "nextval('project_subject_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('description', sa.TEXT(), + autoincrement=False, nullable=True), + sa.Column('max_team_size', sa.INTEGER(), server_default=sa.text( + '4'), autoincrement=False, nullable=False), + sa.CheckConstraint('deadline >= CURRENT_DATE', + name='deadline_check'), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='project_subject_id_fkey', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name='project_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('bestand', + sa.Column('bestand_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.Column('indiening_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['indiening_id'], [ + 'indiening.indiening_id'], name='bestand_indiening_id_fkey', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('bestand_id', name='bestand_pkey') + ) + op.create_table('websiteuser', + sa.Column('azureobjectid', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('is_admin', sa.BOOLEAN(), server_default=sa.text( + 'false'), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('azureobjectid', name='websiteuser_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('student_group', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('team_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['team_id'], ['team.id'], + name='student_group_team_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='student_group_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'team_id', name='student_group_pkey') + ) + op.create_table('teacher_subject', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='teacher_subject_subject_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='teacher_subject_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'subject_id', + name='teacher_subject_pkey') + ) + op.create_table('lesgevervak', + sa.Column('azureobjectid', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('vak_id', sa.TEXT(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['azureobjectid'], [ + 'websiteuser.azureobjectid'], name='lesgevervak_azureobjectid_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['vak_id'], ['vak.vak_id'], + name='lesgevervak_vak_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint( + 'azureobjectid', 'vak_id', name='lesgevervak_pkey') + ) + op.create_table('indiening', + sa.Column('indiening_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.Column('datum', postgresql.TIMESTAMP(), server_default=sa.text( + 'CURRENT_TIMESTAMP'), autoincrement=False, nullable=False), + sa.Column('groep_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('status_id', sa.BIGINT(), server_default=sa.text( + '1'), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['groep_id'], ['groep.groep_id'], + name='indiening_groep_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('indiening_id', name='indiening_pkey') + ) + op.create_table('groep', + sa.Column('groep_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.Column('groep', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('score', sa.BIGINT(), autoincrement=False, nullable=True), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.CheckConstraint('score >= 0 AND score <= 20', + name='score_check'), + sa.PrimaryKeyConstraint('groep_id', name='groep_pkey') + ) + op.create_table('team', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('team_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('score', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.CheckConstraint('score >= 0 AND score <= 20', + name='score_check'), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='team_project_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='team_pkey') + ) + op.create_table('studentvak', + sa.Column('azureobjectid', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('vak_id', sa.TEXT(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['azureobjectid'], [ + 'websiteuser.azureobjectid'], name='studentvak_azureobjectid_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['vak_id'], ['vak.vak_id'], + name='studentvak_vak_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint( + 'azureobjectid', 'vak_id', name='studentvak_pkey') + ) + op.create_table('student_subject', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='student_subject_subject_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='student_subject_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'subject_id', + name='student_subject_pkey') + ) + # ### end Alembic commands ### diff --git a/backend/alembic/versions/eb472f05f70e_recreate_tables.py b/backend/alembic/versions/eb472f05f70e_recreate_tables.py new file mode 100644 index 00000000..eee42a68 --- /dev/null +++ b/backend/alembic/versions/eb472f05f70e_recreate_tables.py @@ -0,0 +1,71 @@ +"""recreate tables + +Revision ID: eb472f05f70e +Revises: 3756e9987aa1 +Create Date: 2024-03-11 18:51:19.501228 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'eb472f05f70e' +down_revision: Union[str, None] = '3756e9987aa1' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('subject', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('website_user', + sa.Column('uid', sa.String(), nullable=False), + sa.Column('given_name', sa.String(), nullable=False), + sa.Column('mail', sa.String(), nullable=False), + sa.Column('is_admin', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('uid') + ) + op.create_table('project', + sa.Column('id', sa.BigInteger(), + autoincrement=True, nullable=False), + sa.Column('deadline', sa.Date(), nullable=False, + check_constraint='deadline >= CURRENT_DATE'), + sa.Column('name', sa.String(), nullable=False), + sa.Column('subjectId', sa.String(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ['subjectId'], ['subject.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_project_id'), 'project', ['id'], unique=False) + op.create_table('student_subject', + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) + op.create_table('teacher_subject', + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('teacher_subject') + op.drop_table('student_subject') + op.drop_index(op.f('ix_project_id'), table_name='project') + op.drop_table('project') + op.drop_table('website_user') + op.drop_table('subject') + # ### end Alembic commands ### diff --git a/backend/alembic/versions/ecbdc859aca6_update_project_scheme.py b/backend/alembic/versions/ecbdc859aca6_update_project_scheme.py new file mode 100644 index 00000000..8cf4a47b --- /dev/null +++ b/backend/alembic/versions/ecbdc859aca6_update_project_scheme.py @@ -0,0 +1,68 @@ +"""update project scheme + +Revision ID: ecbdc859aca6 +Revises: 29a17fa183de +Create Date: 2024-03-12 16:43:51.794097 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'ecbdc859aca6' +down_revision: Union[str, None] = '29a17fa183de' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('project', 'id', + existing_type=sa.BIGINT(), + type_=sa.Integer(), + existing_nullable=False, + autoincrement=True) + op.alter_column('project', 'deadline', + existing_type=sa.DATE(), + type_=sa.DateTime(), + existing_nullable=False) + op.alter_column('project', 'subject_id', + existing_type=sa.BIGINT(), + type_=sa.Integer(), + existing_nullable=True) + op.alter_column('project', 'enroll_deadline', + existing_type=sa.DATE(), + type_=sa.DateTime(), + existing_nullable=True) + op.drop_constraint('project_subject_id_fkey', 'project', type_='foreignkey') + op.create_foreign_key(None, 'project', 'subject', [ + 'subject_id'], ['id'], ondelete='CASCADE') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'project', type_='foreignkey') # type: ignore + op.create_foreign_key('project_subject_id_fkey', 'project', 'subject', [ + 'subject_id'], ['id'], ondelete='SET NULL') + op.alter_column('project', 'enroll_deadline', + existing_type=sa.DateTime(), + type_=sa.DATE(), + existing_nullable=True) + op.alter_column('project', 'subject_id', + existing_type=sa.Integer(), + type_=sa.BIGINT(), + existing_nullable=True) + op.alter_column('project', 'deadline', + existing_type=sa.DateTime(), + type_=sa.DATE(), + existing_nullable=False) + op.alter_column('project', 'id', + existing_type=sa.Integer(), + type_=sa.BIGINT(), + existing_nullable=False, + autoincrement=True) + # ### end Alembic commands ### diff --git a/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py b/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py new file mode 100644 index 00000000..367b0dee --- /dev/null +++ b/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py @@ -0,0 +1,70 @@ +"""Correct project subjectId type + +Revision ID: fc317e930257 +Revises: eb472f05f70e +Create Date: 2024-03-11 19:07:21.468978 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'fc317e930257' +down_revision: Union[str, None] = 'eb472f05f70e' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('subject', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('website_user', + sa.Column('uid', sa.String(), nullable=False), + sa.Column('given_name', sa.String(), nullable=False), + sa.Column('mail', sa.String(), nullable=False), + sa.Column('is_admin', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('uid') + ) + op.create_table('project', + sa.Column('id', sa.BigInteger(), + autoincrement=True, nullable=False), + sa.Column('deadline', sa.Date(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('subject_id', sa.BigInteger(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.CheckConstraint('deadline >= CURRENT_DATE', + name='deadline_check'), + sa.ForeignKeyConstraint( + ['subject_id'], ['subject.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('student_subject', + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) + op.create_table('teacher_subject', + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('teacher_subject') + op.drop_table('student_subject') + op.drop_table('project') + op.drop_table('website_user') + op.drop_table('subject') + # ### end Alembic commands ### diff --git a/backend/requirements.txt b/backend/requirements.txt index 959c0992..bc56e132 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,5 +1,7 @@ +alembic==1.13.1 annotated-types==0.6.0 anyio==4.3.0 +asyncpg==0.29.0 attrs==23.1.0 autopep8==2.0.4 cattrs==23.1.2 @@ -20,9 +22,13 @@ iniconfig==2.0.0 itsdangerous==2.1.2 lsprotocol==2023.0.0a2 lxml==5.1.0 +Mako==1.3.2 +MarkupSafe==2.1.5 nodeenv==1.8.0 packaging==24.0 pluggy==1.4.0 +psycopg-binary==3.1.18 +psycopg-pool==3.2.1 psycopg2-binary==2.9.9 pycodestyle==2.11.1 pycparser==2.21 diff --git a/backend/src/database.py b/backend/src/database.py index 21bbb78e..02b1c5c6 100644 --- a/backend/src/database.py +++ b/backend/src/database.py @@ -1,12 +1,17 @@ from sqlalchemy import MetaData, create_engine +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.orm import declarative_base from sqlalchemy.orm import sessionmaker from src import config SQLALCHEMY_DATABASE_URL = config.CONFIG.database_uri +# TODO: migrate full codebase to async engine = create_engine(SQLALCHEMY_DATABASE_URL) +async_engine = create_async_engine(SQLALCHEMY_DATABASE_URL[:len( + "postgresql")] + "+asyncpg" + SQLALCHEMY_DATABASE_URL[len("postgresql"):]) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +AsyncSessionLocal = async_sessionmaker(async_engine, autoflush=False) Base = declarative_base() diff --git a/backend/src/dependencies.py b/backend/src/dependencies.py index b1c37e2d..e3a24eba 100644 --- a/backend/src/dependencies.py +++ b/backend/src/dependencies.py @@ -1,4 +1,4 @@ -from .database import SessionLocal +from .database import SessionLocal, AsyncSessionLocal def get_db(): @@ -8,3 +8,12 @@ def get_db(): yield db finally: db.close() + + +async def get_async_db(): + """Creates new async database session per request, which is closed afterwards""" + db = AsyncSessionLocal() + try: + yield db + finally: + await db.close() diff --git a/backend/src/main.py b/backend/src/main.py index 18010993..6e649a70 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -2,9 +2,11 @@ from starlette.middleware.sessions import SessionMiddleware from src.subject.router import router as subject_router from src.user.router import router as user_router +from src.project.router import router as project_router from src.auth.router import router as auth_router from fastapi.middleware.cors import CORSMiddleware from src import config +from src.project.router import router as project_router app = FastAPI() @@ -21,6 +23,7 @@ app.include_router(subject_router) app.include_router(user_router) +app.include_router(project_router) app.include_router(auth_router) diff --git a/backend/src/project/dependencies.py b/backend/src/project/dependencies.py new file mode 100644 index 00000000..ecbc71d2 --- /dev/null +++ b/backend/src/project/dependencies.py @@ -0,0 +1,19 @@ +from fastapi import Depends +from sqlalchemy.orm import Session +from src.dependencies import get_db +from src.user.dependencies import get_authenticated_user +from src.user.schemas import User + +from ..auth.exceptions import NotAuthorized +from ..subject.service import get_teachers + + +async def user_permission_validation( + subject_id: int, + user: User = Depends(get_authenticated_user), + db: Session = Depends(get_db), +): + if not user.is_admin: + teachers = await get_teachers(db, subject_id) + if not list(filter(lambda teacher: teacher.id == user.uid, teachers)): + raise NotAuthorized() diff --git a/backend/src/project/exceptions.py b/backend/src/project/exceptions.py new file mode 100644 index 00000000..ed3c4d5c --- /dev/null +++ b/backend/src/project/exceptions.py @@ -0,0 +1,5 @@ +from fastapi import HTTPException + + +def ProjectNotFoundException(): + return HTTPException(status_code=404, detail="Project not found") diff --git a/backend/src/project/models.py b/backend/src/project/models.py new file mode 100644 index 00000000..237b12d9 --- /dev/null +++ b/backend/src/project/models.py @@ -0,0 +1,22 @@ +from datetime import datetime +from sqlalchemy import BigInteger, DateTime, ForeignKey, CheckConstraint +from sqlalchemy.orm import Mapped, mapped_column +from src.database import Base + + +class Project(Base): + __tablename__ = 'project' + + id: Mapped[int] = mapped_column(primary_key=True) + deadline: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) + name: Mapped[str] = mapped_column(nullable=False) + subject_id: Mapped[int] = mapped_column(ForeignKey( + 'subject.id', ondelete="CASCADE"), nullable=True) + description: Mapped[str] = mapped_column(nullable=True) + + enroll_deadline: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=True) + + __table_args__ = ( + CheckConstraint('deadline >= CURRENT_DATE', name='deadline_check'), + ) diff --git a/backend/src/project/router.py b/backend/src/project/router.py new file mode 100644 index 00000000..bccad258 --- /dev/null +++ b/backend/src/project/router.py @@ -0,0 +1,67 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession +from src.subject.dependencies import user_permission_validation +from src.dependencies import get_async_db, get_db + +from .exceptions import ProjectNotFoundException +from .schemas import ProjectCreate, ProjectResponse, ProjectUpdate +from .service import create_project, get_project, delete_project, update_project, get_projects_for_subject + +router = APIRouter( + prefix="/api/subjects/{subject_id}/projects", + tags=["projects"], + responses={404: {"description": "Not found"}}, +) + + +@router.get("/", response_model=list[ProjectResponse]) +async def list_projects_for_subject( + subject_id: int, + db: AsyncSession = Depends(get_async_db) +): + projects = await get_projects_for_subject(db, subject_id) + return projects + + +@router.post("/", + response_model=ProjectResponse, + dependencies=[Depends(user_permission_validation)], + status_code=201) +async def create_project_for_subject( + subject_id: int, + project_in: ProjectCreate, + db: AsyncSession = Depends(get_async_db) +): + project = await create_project(db, project_in, subject_id) + return project + + +@router.get("/{project_id}", response_model=ProjectResponse) +async def get_project_for_subject( + project_id: int, + db: AsyncSession = Depends(get_async_db) +): + project = await get_project(db, project_id) + if not project: + raise ProjectNotFoundException() + return project + + +@router.delete("/{project_id}", dependencies=[Depends(user_permission_validation)]) +async def delete_project_for_subject( + project_id: int, + db: AsyncSession = Depends(get_async_db) +): + await delete_project(db, project_id) + return {"message": "Project deleted successfully"} + + +@router.patch("/{project_id}", + response_model=ProjectResponse, + dependencies=[Depends(user_permission_validation)]) +async def patch_project_for_subject( + project_id: int, + project_update: ProjectUpdate, + db: AsyncSession = Depends(get_async_db) +): + return await update_project(db, project_id, project_update) diff --git a/backend/src/project/schemas.py b/backend/src/project/schemas.py new file mode 100644 index 00000000..d7a09e2a --- /dev/null +++ b/backend/src/project/schemas.py @@ -0,0 +1,38 @@ +from datetime import datetime, date, timezone +from typing import Optional + +from pydantic import BaseModel, Field, validator, ConfigDict, field_validator + + +class ProjectCreate(BaseModel): + name: str = Field(..., min_length=1) + deadline: datetime + description: str + + # Check if deadline is not in the past + @field_validator('deadline') + def validate_deadline(cls, value: datetime) -> datetime: + if value < datetime.now(value.tzinfo): + raise ValueError('The deadline cannot be in the past') + return value + + +class ProjectResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + name: str + deadline: datetime + description: str + + +class ProjectUpdate(BaseModel): + name: Optional[str] = Field(None, min_length=1) + deadline: Optional[datetime] = None + description: Optional[str] = None + + @field_validator('deadline') + def validate_deadline(cls, value: datetime) -> datetime: + if value is not None and value < datetime.now(value.tzinfo): + raise ValueError('The deadline cannot be in the past') + return value diff --git a/backend/src/project/service.py b/backend/src/project/service.py new file mode 100644 index 00000000..a2329002 --- /dev/null +++ b/backend/src/project/service.py @@ -0,0 +1,57 @@ +from typing import List, Sequence +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select +from . import models +from .exceptions import ProjectNotFoundException +from .models import Project +from .schemas import ProjectCreate, ProjectUpdate + + +async def create_project(db: AsyncSession, project_in: ProjectCreate, subject_id: int) -> Project: + new_project = Project( + name=project_in.name, + deadline=project_in.deadline, + subject_id=subject_id, + description=project_in.description + ) + db.add(new_project) + await db.commit() + await db.refresh(new_project) + return new_project + + +async def get_project(db: AsyncSession, project_id: int) -> models.Project: + result = await db.execute(select(models.Project).filter(models.Project.id == project_id)) + return result.scalars().first() + + +async def get_projects_for_subject(db: AsyncSession, subject_id: int) -> Sequence[models.Project]: + result = await db.execute(select(models.Project).filter_by(subject_id=subject_id)) + projects = result.scalars().all() + return projects + + +async def delete_project(db: AsyncSession, project_id: int): + result = await db.execute(select(models.Project).filter(models.Project.id == project_id)) + project = result.scalars().first() + if project: + await db.delete(project) + await db.commit() + + +async def update_project(db: AsyncSession, project_id: int, project_update: ProjectUpdate) -> Project: + result = await db.execute(select(Project).filter(Project.id == project_id)) + project = result.scalars().first() + if not project: + raise ProjectNotFoundException() + + if project_update.name is not None: + project.name = project_update.name + if project_update.deadline is not None: + project.deadline = project_update.deadline + if project_update.description is not None: + project.description = project_update.description + + await db.commit() + await db.refresh(project) + return project diff --git a/backend/src/subject/models.py b/backend/src/subject/models.py index 1b864e13..ad7371db 100644 --- a/backend/src/subject/models.py +++ b/backend/src/subject/models.py @@ -5,14 +5,14 @@ StudentSubject = Table( "student_subject", Base.metadata, - Column("uid", ForeignKey("user.uid")), + Column("uid", ForeignKey("website_user.uid")), Column("subject_id", ForeignKey("subject.id")), ) TeacherSubject = Table( "teacher_subject", Base.metadata, - Column("uid", ForeignKey("user.uid")), + Column("uid", ForeignKey("website_user.uid")), Column("subject_id", ForeignKey("subject.id")), ) diff --git a/backend/src/subject/service.py b/backend/src/subject/service.py index 981fe15b..b42ad276 100644 --- a/backend/src/subject/service.py +++ b/backend/src/subject/service.py @@ -1,4 +1,7 @@ from typing import Sequence + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session from . import models, schemas from src.user.models import User