diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1ea7ee0 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ + +# FreeIPA +IPA_SERVER_HOSTNAME= +IPA_PASSWORD= + +# PostgreSQL +POSTGRES_DB= +POSTGRES_USER= +POSTGRES_PASSWORD= + +# tMatch +LDAP_HOST= +LDAP_PORT= +LDAP_USER= +LDAP_PASSWORD= +LDAP_BASE_DN= + +SMTP_SERVER= +SMTP_PORT= +SMTP_USERNAME= +SMTP_PASSWORD= diff --git a/.gitignore b/.gitignore index d1ca65b..4f802ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +.env +srv/ certs/ src/**/__pycache__ src/specs/* diff --git a/compose.yml b/compose.yml index dfae74d..0c9a10a 100644 --- a/compose.yml +++ b/compose.yml @@ -1,124 +1,83 @@ - services: freeipa: image: freeipa/freeipa-server:almalinux-10 container_name: freeipa-server - hostname: ipa.example.test + hostname: auth-tmatch.isc-vs.dev tty: true stdin_open: true - + privileged: false - cap_add: - - SYS_ADMIN - security_opt: - - seccomp=unconfined - - apparmor=unconfined - - cgroup: host - + environment: - - IPA_SERVER_HOSTNAME=ipa.example.test - - PASSWORD=thaitiePh5Haevie0cah - - SYSTEMD_SECCOMP=0 - + - IPA_SERVER_HOSTNAME=${IPA_SERVER_HOSTNAME} + - PASSWORD=${IPA_PASSWORD} + volumes: - - ./ipa_data:/data:Z - - /sys/fs/cgroup:/sys/fs/cgroup:rw - + - ./srv/freeipa-data:/data:Z + tmpfs: - /run - /tmp - - /var/lib/journal - - /run/lock - + ports: - - "3180:80" - - "31443:443" - - "31389:389" - - "31636:636" - - "3188:88" - - "3188:88/udp" - - "31464:464" - - "31464:464/udp" - + - "12343:443" + - "12389:389" + - "12336:636" + command: - ipa-server-install - -U - -r - - EXAMPLE.TEST + - TMATCH.ISC-VS.DEV - --domain - - example.test + - tmatch.isc-vs.dev - --no-ntp - --no-host-dns - --skip-mem-check - - restart: unless-stopped - - keycloak: - image: quay.io/keycloak/keycloak:26.5.3 - container_name: keycloak-server - environment: - - KC_BOOTSTRAP_ADMIN_USERNAME=admin - - KC_BOOTSTRAP_ADMIN_PASSWORD=admin - - KC_DB=postgres - - KC_DB_URL_HOST=keycloak-db - - KC_DB_URL_DATABASE=keycloak - - KC_DB_USERNAME=keycloak - - KC_DB_PASSWORD=0291040404 - - KC_HOSTNAME=ipa.example.test - - KC_HTTPS_CERTIFICATE_FILE=/opt/keycloak/conf/server.crt - - KC_HTTPS_CERTIFICATE_KEY_FILE=/opt/keycloak/conf/server.key - volumes: - - /srv/tmatch/keycloak/providers:/opt/keycloak/providers - - /srv/tmatch/keycloak/themes:/opt/keycloak/themes - - ./certs/server.crt:/opt/keycloak/conf/server.crt - - ./certs/server.key:/opt/keycloak/conf/server.key - ports: - - "3181:8080" - - "31443:8443" - command: - - start - restart: unless-stopped - depends_on: - - keycloak-db - - freeipa - keycloak-db: - image: postgres:16-alpine - container_name: keycloak_db - environment: - - POSTGRES_DB=keycloak - - POSTGRES_USER=keycloak - - POSTGRES_PASSWORD=nieM1ui7ak9sheitohsh - volumes: - - ./keycloak_data:/var/lib/postgresql/data restart: unless-stopped tmatch_db: image: postgres:16-alpine container_name: tmatch_db environment: - - POSTGRES_DB=tmatch_db - - POSTGRES_USER=tmatch - - POSTGRES_PASSWORD=Oochoghahp6ie7aiza9p + - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} ports: - "3132:5432" restart: unless-stopped volumes: - - tmatch_db_data:/var/lib/postgresql/data + - ./srv/tmatch_db_data:/var/lib/postgresql/data tmatch_app: build: . container_name: tmatch_app restart: unless-stopped + environment: + - LDAP_HOST=${LDAP_HOST} + - LDAP_PORT=${LDAP_PORT} + - LDAP_USER=${LDAP_USER} + - LDAP_PASSWORD=${LDAP_PASSWORD} + - LDAP_BASE_DN=${LDAP_BASE_DN} + + - SMTP_SERVER=${SMTP_SERVER} + - SMTP_PORT=${SMTP_PORT} + - SMTP_USERNAME=${SMTP_USERNAME} + - SMTP_PASSWORD=${SMTP_PASSWORD} + + - DB_NAME=${POSTGRES_DB} + - DB_USER=${POSTGRES_USER} + - DB_PASSWORD=${POSTGRES_PASSWORD} ports: - "8084:8501" depends_on: - tmatch_db - freeipa volumes: - - tmatch_specs_data:/app/specs + - ./srv/tmatch_specs_data:/app/specs volumes: - tmatch_specs_data: - tmatch_db_data: + tmatch_specs_data: + tmatch_db_data: + tmatch_keycloak_data: diff --git a/dockerfile b/dockerfile index 711b62b..3367660 100644 --- a/dockerfile +++ b/dockerfile @@ -12,9 +12,12 @@ WORKDIR /app COPY src/pyproject.toml src/uv.lock . RUN uv sync --frozen --no-install-project -COPY src/* . +COPY src/ . RUN uv sync --frozen -CMD ["uv", "run", "streamlit", "run", "main.py"] +COPY entrypoint.sh ./ +RUN chmod +x entrypoint.sh + +CMD ["sh", "./entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..b622abc --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,12 @@ + +#!/bin/sh +set -e + +echo "Running migrations..." +uv run alembic upgrade head + +echo "Seeding database..." +uv run python seed.py + +echo "Starting app..." +exec uv run streamlit run main.py diff --git a/src/alembic/env.py b/src/alembic/env.py index 55c0ce3..5e3c004 100644 --- a/src/alembic/env.py +++ b/src/alembic/env.py @@ -5,12 +5,15 @@ from alembic import context +from config import DATABASE_URL from models import Base # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config +config.set_main_option("sqlalchemy.url", DATABASE_URL) + # Interpret the config file for Python logging. # This line sets up loggers basically. if config.config_file_name is not None: diff --git a/src/app.py b/src/app.py index 912cd3e..b90493c 100644 --- a/src/app.py +++ b/src/app.py @@ -2,7 +2,7 @@ import streamlit as st from services.auth import validate_session from services.db import get_db -from utils.nav import PAGE_ROLES, PAGE_CONFIG, allowed, landing_page, login_page +from utils.nav import PAGE_ROLES, PAGE_CONFIG, allowed, projects_page, login_page st.session_state.session = validate_session() @@ -50,15 +50,15 @@ def set_program(): st.session_state.program_id = program_id if len(roles) == 0: - page_list = [landing_page] + page_list = [projects_page] else: - page_list = [landing_page] + [ + page_list = [ PAGE_CONFIG[page_name] for page_name, allowed_roles in PAGE_ROLES.items() if allowed(roles, allowed_roles) ] if not page_list: - page_list = [landing_page] + page_list = [projects_page] else: page_list = [login_page] diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..53ff270 --- /dev/null +++ b/src/config.py @@ -0,0 +1,25 @@ +import os + +# PostgreSQL +DB_HOST = "tmatch_db" +DB_PORT = 5432 +DB_NAME = os.environ.get("DB_NAME", "") +DB_USER = os.environ.get("DB_USER", "") +DB_PASSWORD = os.environ.get("DB_PASSWORD", "") + +DATABASE_URL = ( + f"postgresql+psycopg://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" +) + +# LDAP +LDAP_HOST = os.environ.get("LDAP_HOST", "") +LDAP_PORT = os.environ.get("LDAP_PORT", "") +LDAP_USER = os.environ.get("LDAP_USER", "") +LDAP_PASSWORD = os.environ.get("LDAP_PASSWORD", "") +LDAP_BASE_DN = os.environ.get("LDAP_BASE_DN", "") + +# Email +SMTP_SERVER = os.environ.get("SMTP_SERVER", "") +SMTP_PORT = int(os.environ.get("SMTP_PORT", 587)) +SMTP_USERNAME = os.environ.get("SMTP_USERNAME", "") +SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD", "") diff --git a/src/seed.py b/src/seed.py new file mode 100644 index 0000000..cbae50b --- /dev/null +++ b/src/seed.py @@ -0,0 +1,25 @@ + +from sqlalchemy.orm import sessionmaker +from models.role import Role +from seeds.keywords import create_keywords +from seeds.programs import create_programs +from seeds.roles import create_roles + +from seeds.engine import engine + +Session = sessionmaker(bind=engine) +session = Session() + +with session as s: + if s.query(Role).count() > 0: + print("Database already seeded. Skipping.") + exit(0) + +create_roles() +print("Created roles") + +create_keywords() +print("Created keywords") + +create_programs() +print("Created programs") diff --git a/src/seed/engine.py b/src/seed/engine.py deleted file mode 100644 index c6e53c1..0000000 --- a/src/seed/engine.py +++ /dev/null @@ -1,4 +0,0 @@ - -from sqlalchemy import create_engine - -engine = create_engine("postgresql+psycopg://postgres:postgres@localhost/tmatch_db") diff --git a/src/seed/roles.py b/src/seed/roles.py deleted file mode 100644 index a71ea65..0000000 --- a/src/seed/roles.py +++ /dev/null @@ -1,20 +0,0 @@ -from sqlalchemy.orm import sessionmaker - -from models.role import Role -from seed.engine import engine - -Session = sessionmaker(bind=engine) -session = Session() - -def create_roles(): - student = Role(name="student") - teacher = Role(name="teacher") - secretary = Role(name="secretary") - program_director = Role(name="program director") - - session.add(student) - session.add(teacher) - session.add(secretary) - session.add(program_director) - - session.commit() diff --git a/src/seed/seed.py b/src/seed/seed.py deleted file mode 100644 index 2360eb0..0000000 --- a/src/seed/seed.py +++ /dev/null @@ -1,13 +0,0 @@ - -from seed.keywords import create_keywords -from seed.roles import create_roles - -print("This is the seed script. It should only be run when the database is freshly created.") -print("It will insert basic keywords and required roles inside the database.") -input("Please press enter to continue.") - -create_roles() -print("Created roles") - -create_keywords() -print("Created keywords") diff --git a/src/seeds/engine.py b/src/seeds/engine.py new file mode 100644 index 0000000..e955974 --- /dev/null +++ b/src/seeds/engine.py @@ -0,0 +1,5 @@ + +from sqlalchemy import create_engine +from config import DATABASE_URL + +engine = create_engine(DATABASE_URL) diff --git a/src/seed/keywords.py b/src/seeds/keywords.py similarity index 91% rename from src/seed/keywords.py rename to src/seeds/keywords.py index 1e5436b..380ccbd 100644 --- a/src/seed/keywords.py +++ b/src/seeds/keywords.py @@ -1,10 +1,9 @@ from sqlalchemy.orm import sessionmaker from models.keyword import Keyword -from seed.engine import engine +from seeds.engine import engine Session = sessionmaker(bind=engine) -session = Session() def create_keywords(): keywords = [ @@ -86,8 +85,10 @@ def create_keywords(): "forecasting", ] - for i in range(len(keywords)): - new_keyword = Keyword(name=keywords[i]) + with Session() as s: + for i in range(len(keywords)): + new_keyword = Keyword(name=keywords[i]) - session.add(new_keyword) - session.commit() + s.add(new_keyword) + + s.commit() diff --git a/src/seeds/programs.py b/src/seeds/programs.py new file mode 100644 index 0000000..797ad06 --- /dev/null +++ b/src/seeds/programs.py @@ -0,0 +1,22 @@ + +from sqlalchemy.orm import sessionmaker + +from models.program import Program +from seeds.engine import engine + +Session = sessionmaker(bind=engine) + +def create_programs(): + programs = [ + "ISC", + "SYND", + "LSE", + "ETE" + ] + + with Session() as s: + for i in range(len(programs)): + new_program = Program(name=programs[i]) + s.add(new_program) + + s.commit() diff --git a/src/seeds/roles.py b/src/seeds/roles.py new file mode 100644 index 0000000..d183990 --- /dev/null +++ b/src/seeds/roles.py @@ -0,0 +1,20 @@ +from sqlalchemy.orm import sessionmaker + +from models.role import Role +from seeds.engine import engine + +Session = sessionmaker(bind=engine) + +def create_roles(): + with Session() as s: + student = Role(name="student") + teacher = Role(name="teacher") + secretary = Role(name="secretary") + program_director = Role(name="program director") + + s.add(student) + s.add(teacher) + s.add(secretary) + s.add(program_director) + + s.commit() diff --git a/src/services/db.py b/src/services/db.py index 0001686..4c89a24 100644 --- a/src/services/db.py +++ b/src/services/db.py @@ -1,10 +1,11 @@ from collections.abc import Sequence from datetime import datetime, timedelta, timezone -from sqlalchemy import select +from sqlalchemy import create_engine, select from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import sessionmaker import streamlit as st -from streamlit.connections import SQLConnection +from config import DATABASE_URL from models.auth_token import AuthToken from models.base import Base from models.program import Program @@ -18,14 +19,16 @@ from models.keyword import Keyword from models.projects_keyword import ProjectsKeyword +@st.cache_resource +def get_session_factory(): + engine = create_engine(DATABASE_URL) + return sessionmaker(bind=engine) class Db: """Database service class for CRUD operations.""" - _conn: SQLConnection - def __init__(self) -> None: - self._conn = st.connection("tmatch_db", type="sql") + self._session_factory = get_session_factory() def create_project(self, created_by: int, teacher_id: int, title: str, description: str, specifications: str, program_id: int) -> Project | None: """Create a new project in the database. @@ -42,7 +45,7 @@ def create_project(self, created_by: int, teacher_id: int, title: str, descripti The created Project object, or None if a database integrity error occurs. """ - with self._conn.session as s: + with self._session_factory() as s: project = Project( created_by=created_by, teacher_id=teacher_id, @@ -73,7 +76,7 @@ def create_session(self, user_id: int) -> Session: The created Session object. """ - with self._conn.session as s: + with self._session_factory() as s: session = Session( user_id=user_id, expires_at=datetime.now(timezone.utc) + timedelta(days=7), @@ -95,7 +98,7 @@ def create_auth_token(self, user_id: int) -> AuthToken: The created AuthToken object. """ - with self._conn.session as s: + with self._session_factory() as s: auth_token = AuthToken( user_id=user_id, expires_at=datetime.now(timezone.utc) + timedelta(seconds=30), @@ -122,7 +125,7 @@ def apply_rating(self, project_id: int, student_id: int, rating: int) -> Project The created or updated ProjectRating object. """ - with self._conn.session as s: + with self._session_factory() as s: existing = s.execute( select(ProjectRating) .where(ProjectRating.student_id == student_id) @@ -156,7 +159,7 @@ def assign_project(self, project_id: int, student_id: int) -> None: student_id: ID of the student to assign the project to. """ - with self._conn.session as s: + with self._session_factory() as s: project = s.execute( select(Project).where(Project.id == project_id) ).scalar_one() @@ -178,7 +181,7 @@ def get_rating(self, project_id: int, student_id: int) -> ProjectRating|None: The ProjectRating object, or None if not found. """ - with self._conn.session as s: + with self._session_factory() as s: return s.execute(select(ProjectRating) .where(ProjectRating.student_id == student_id) .where(ProjectRating.project_id == project_id) @@ -194,7 +197,7 @@ def get_ratings(self, program_id: int) -> Sequence[ProjectRating]: Sequence of all ProjectRating objects for the program. """ - with self._conn.session as s: + with self._session_factory() as s: return ( s.execute( select(ProjectRating) @@ -215,7 +218,7 @@ def get_projects(self, program_id: int) -> Sequence[Project]: Sequence of all Project objects for the program. """ - with self._conn.session as s: + with self._session_factory() as s: return ( s.execute( select(Project) @@ -236,7 +239,7 @@ def get_teachers(self, program_id: int) -> Sequence[User]: Sequence of all User objects with teacher role in the program. """ - with self._conn.session as s: + with self._session_factory() as s: return ( s.execute( select(User) @@ -261,7 +264,7 @@ def get_students(self, program_id: int) -> Sequence[User]: Sequence of all User objects with student role in the program. """ - with self._conn.session as s: + with self._session_factory() as s: return ( s.execute( select(User) @@ -283,7 +286,7 @@ def get_keywords(self) -> Sequence[Keyword]: Sequence of all Keyword objects. """ - with self._conn.session as s: + with self._session_factory() as s: return s.execute(select(Keyword)).scalars().all() def update_project_keywords(self, project_id: int, keyword_ids: list[int]) -> None: @@ -294,7 +297,7 @@ def update_project_keywords(self, project_id: int, keyword_ids: list[int]) -> No keyword_ids: List of keyword IDs to associate with the project. """ - with self._conn.session as s: + with self._session_factory() as s: s.execute( delete(ProjectsKeyword) .where(ProjectsKeyword.project_id == project_id) @@ -310,7 +313,7 @@ def get_users(self) -> Sequence[User]: Sequence of all User objects. """ - with self._conn.session as s: + with self._session_factory() as s: return ( s.execute( select(User) @@ -329,7 +332,7 @@ def get_programs(self) -> Sequence[Program]: Sequence of all Program objects. """ - with self._conn.session as s: + with self._session_factory() as s: return s.execute(select(Program)).scalars().all() def get_roles(self) -> Sequence[Role]: @@ -339,7 +342,7 @@ def get_roles(self) -> Sequence[Role]: Sequence of all Role objects. """ - with self._conn.session as s: + with self._session_factory() as s: return s.execute(select(Role)).scalars().all() def update_user_role(self, user_id: int, program_id: int, role_name: str) -> None: @@ -351,7 +354,7 @@ def update_user_role(self, user_id: int, program_id: int, role_name: str) -> Non role_name: Name of the role to assign. """ - with self._conn.session as s: + with self._session_factory() as s: role = s.execute( select(Role).where(Role.name == role_name) ).scalar_one_or_none() @@ -379,7 +382,7 @@ def get_user(self, uid: str) -> User: The User object (existing or newly created). """ - with self._conn.session as s: + with self._session_factory() as s: user = s.execute( select(User).where(User.ldap_uid == uid) ).scalar_one_or_none() @@ -404,7 +407,7 @@ def get_project(self, project_id: int) -> Project|None: The Project object, or None if not found. """ - with self._conn.session as s: + with self._session_factory() as s: return s.execute( select(Project).where(Project.id == project_id) ).scalar_one_or_none() @@ -419,7 +422,7 @@ def get_session(self, sid: str) -> Session | None: The Session object, or None if not found. """ - with self._conn.session as s: + with self._session_factory() as s: return s.execute( select(Session).where(Session.id == sid) ).scalar_one_or_none() @@ -434,7 +437,7 @@ def get_auth_token(self, token_id: str) -> AuthToken | None: The AuthToken object, or None if not found. """ - with self._conn.session as s: + with self._session_factory() as s: return s.execute( select(AuthToken).where(AuthToken.id == token_id) ).scalar_one_or_none() @@ -449,7 +452,7 @@ def get_program(self, program_id: int) -> Program | None: The Program object, or None if not found. """ - with self._conn.session as s: + with self._session_factory() as s: return s.execute( select(Program).where(Program.id == program_id) ).scalar_one_or_none() @@ -467,7 +470,7 @@ def update_project(self, project_id: int, title: str, description: str, teacher_ The updated Project object, or None if not found or on integrity error. """ - with self._conn.session as s: + with self._session_factory() as s: project = s.execute( select(Project).where(Project.id == project_id) ).scalar_one_or_none() @@ -494,7 +497,7 @@ def remove(self, model: Base) -> None: Cascade behavior depends on the model configuration. """ - with self._conn.session as s: + with self._session_factory() as s: s.delete(model) s.commit() diff --git a/src/services/ldap.py b/src/services/ldap.py index 1c65de6..facbf0c 100644 --- a/src/services/ldap.py +++ b/src/services/ldap.py @@ -1,9 +1,9 @@ -import streamlit as st +from config import LDAP_BASE_DN, LDAP_HOST, LDAP_PASSWORD, LDAP_PORT, LDAP_USER from ldap3 import Entry, Server, Connection, ALL def _get_server(): - return Server(st.secrets.ldap.ldaphost, port=int(st.secrets.ldap.ldapport), use_ssl=True, get_info=ALL) + return Server(LDAP_HOST, port=int(LDAP_PORT), use_ssl=True, get_info=ALL) def _search_user(uid: str, attributes: list[str]) -> Entry | None: @@ -21,13 +21,14 @@ def _search_user(uid: str, attributes: list[str]) -> Entry | None: return None try: server = _get_server() - conn = Connection(server, user=st.secrets.ldap.ldapaccount, password=st.secrets.ldap.ldappassword, auto_bind=True) - conn.search(st.secrets.ldap.ldapbasedn, f"(uid={uid})", "SUBTREE", attributes=attributes) + conn = Connection(server, user=LDAP_USER, password=LDAP_PASSWORD, auto_bind=True) + conn.search(LDAP_BASE_DN, f"(uid={uid})", "SUBTREE", attributes=attributes) entry = conn.entries[0] if conn.entries else None conn.unbind() return entry except Exception as e: print(e) + print("search bind error") return None @@ -69,6 +70,7 @@ def authenticate(uid: str, password: str) -> dict[str, str|None] | None: except Exception as e: print(e) + print("auth bind error") return None diff --git a/src/services/mail.py b/src/services/mail.py index 5ef93c2..07423f5 100644 --- a/src/services/mail.py +++ b/src/services/mail.py @@ -5,9 +5,9 @@ from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart import ssl -import streamlit as st from jinja2 import Environment, FileSystemLoader +from config import SMTP_PASSWORD, SMTP_SERVER, SMTP_PORT, SMTP_USERNAME from models.project import Project from models.user import User from services.db import Db, get_db @@ -34,11 +34,11 @@ class Mailer: _sender: str def __init__(self) -> None: - self._server = st.secrets.mailer.smtpserver - self._port = st.secrets.mailer.smtpserverport - self._username = st.secrets.mailer.smtpusername - self._password = st.secrets.mailer.smtppassword - self._sender = st.secrets.mailer.sender + self._server = SMTP_SERVER + self._port = SMTP_PORT + self._username = SMTP_USERNAME + self._password = SMTP_PASSWORD + self._sender = SMTP_USERNAME def project_supervision(self, project: Project): """Notify a teacher that a project has been assigned to them. diff --git a/src/utils/nav.py b/src/utils/nav.py index 856242d..c94411c 100644 --- a/src/utils/nav.py +++ b/src/utils/nav.py @@ -4,14 +4,12 @@ from models.role import Role login_page = st.Page("views/login.py", title="Login", default=True) -landing_page = st.Page("views/landing.py", title="Home") manage_projects_page = st.Page("views/manage_projects.py", title="Manage Projects") project_detail_page = st.Page("views/project_detail.py", title="View Project Details", visibility="hidden") projects_page = st.Page("views/projects.py", title="View Projects List") assigned_project_page = st.Page("views/assigned_project.py", title="Assigned Project") PAGE_CONFIG = { - "landing": landing_page, "manage_projects": manage_projects_page, "project_detail": project_detail_page, "projects": projects_page, @@ -61,4 +59,4 @@ def protect(page_name: str): allowed_roles = PAGE_ROLES.get(page_name, []) if not allowed(roles, allowed_roles): - st.switch_page(landing_page) + st.switch_page(projects_page) diff --git a/src/views/landing.py b/src/views/landing.py deleted file mode 100644 index 183db3b..0000000 --- a/src/views/landing.py +++ /dev/null @@ -1,10 +0,0 @@ - -import streamlit as st - -from utils.nav import protect - -protect("landing") - -st.write("hello") -st.write() - diff --git a/src/views/projects.py b/src/views/projects.py index f0efc9c..0d1f5ce 100644 --- a/src/views/projects.py +++ b/src/views/projects.py @@ -63,17 +63,20 @@ def render_for_others(): st.divider() - cols = st.columns([4, 0.8]) + cols = st.columns([4, 3, 4, 1.5]) cols[0].markdown("**Project**") - + cols[1].markdown("**Teacher**") st.divider() for project in projects: pid = project.id - cols = st.columns([4, 0.8]) + + cols = st.columns([4, 3, 4, 1.5]) cols[0].write(project.title) + cols[1].write(project.teacher.ldap_uid) - if cols[1].button("Open", key=f"open_{pid}"): + + if cols[2].button("Detail", key=f"detail_{pid}"): st.session_state.selected_project = project.id st.switch_page(project_detail_page)