diff --git a/app/api.py b/app/api.py deleted file mode 100644 index 7a326fe35c..0000000000 --- a/app/api.py +++ /dev/null @@ -1,11 +0,0 @@ -"""File defining all the routes for the module, to configure the router""" - -from fastapi import APIRouter - -from app.module import all_modules - -api_router = APIRouter() - - -for module in all_modules: - api_router.include_router(module.router) diff --git a/app/app.py b/app/app.py index 8b95cd85fe..b563de04f2 100644 --- a/app/app.py +++ b/app/app.py @@ -12,7 +12,7 @@ import alembic.migration as alembic_migration import redis from calypsso import get_calypsso_app -from fastapi import FastAPI, HTTPException, Request, Response, status +from fastapi import APIRouter, FastAPI, HTTPException, Request, Response, status from fastapi.encoders import jsonable_encoder from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware @@ -24,7 +24,6 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from app import api from app.core.core_endpoints import coredata_core, models_core from app.core.google_api.google_api import GoogleAPI from app.core.groups import models_groups @@ -41,7 +40,7 @@ get_redis_client, init_state, ) -from app.module import all_modules, module_list +from app.module import get_all_modules, get_module_list, init_module_list from app.types.exceptions import ( ContentHTTPException, GoogleAPIInvalidCredentialsError, @@ -51,6 +50,7 @@ from app.utils import initialization from app.utils.communication.notifications import NotificationManager from app.utils.redis import limiter +from app.utils.screenshots.factory_screenshots import create_data from app.utils.state import LifespanState if TYPE_CHECKING: @@ -234,13 +234,15 @@ async def run_factories( hyperion_error_logger: logging.Logger, ) -> None: """Run the factories to create default data in the database""" + await create_data(db=db) + if not settings.USE_FACTORIES: return hyperion_error_logger.info("Startup: Factories enabled") # Importing the core_factory at the beginning of the factories. factories_list: list[Factory] = [] - for module in all_modules: + for module in get_all_modules(): if module.factory: factories_list.append(module.factory) hyperion_error_logger.info( @@ -299,10 +301,11 @@ def initialize_module_visibility( coredata_core.ModuleVisibilityAwareness, db, ) + known_roots = module_awareness.roots new_modules = [ module - for module in module_list + for module in get_module_list() if module.root not in module_awareness.roots ] # Is run to create default module visibilities or when the table is empty @@ -311,6 +314,7 @@ def initialize_module_visibility( f"Startup: Some modules visibility settings are empty, initializing them ({[module.root for module in new_modules]})", ) for module in new_modules: + known_roots.append(module.root) if module.default_allowed_groups_ids is not None: for group_id in module.default_allowed_groups_ids: module_group_visibility = models_core.ModuleGroupVisibility( @@ -344,9 +348,7 @@ def initialize_module_visibility( f"Startup: Could not add module visibility {module.root} in the database: {error}", ) initialization.set_core_data_sync( - coredata_core.ModuleVisibilityAwareness( - roots=[module.root for module in module_list], - ), + coredata_core.ModuleVisibilityAwareness(roots=known_roots), db, ) hyperion_error_logger.info( @@ -365,7 +367,7 @@ async def initialize_notification_topics( ) -> None: existing_topics = await get_notification_topic(db=db) existing_topics_id = [topic.id for topic in existing_topics] - for module in all_modules: + for module in get_all_modules(): if module.registred_topics: for registred_topic in module.registred_topics: if registred_topic.id not in existing_topics_id: @@ -609,7 +611,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[LifespanState, None]: app=app, settings=settings, hyperion_error_logger=hyperion_error_logger, - drop_db=drop_db, + drop_db=True, ) yield state @@ -622,13 +624,18 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[LifespanState, None]: hyperion_error_logger=hyperion_error_logger, ) + init_module_list(settings=settings) + # Initialize app app = FastAPI( title="Hyperion", version=settings.HYPERION_VERSION, lifespan=lifespan, ) - app.include_router(api.api_router) + api_router = APIRouter() + for module in get_all_modules(): + api_router.include_router(module.router) + app.include_router(api_router) use_route_path_as_operation_ids(app) app.add_middleware( diff --git a/app/core/checkout/endpoints_checkout.py b/app/core/checkout/endpoints_checkout.py index 4158953344..35e059a477 100644 --- a/app/core/checkout/endpoints_checkout.py +++ b/app/core/checkout/endpoints_checkout.py @@ -14,7 +14,7 @@ NotificationResultContent, ) from app.dependencies import get_db -from app.module import all_modules +from app.module import get_all_modules from app.types.module import CoreModule router = APIRouter(tags=["Checkout"]) @@ -133,7 +133,7 @@ async def webhook( # If a callback is defined for the module, we want to call it try: - for module in all_modules: + for module in get_all_modules(): if module.root == checkout.module: if module.payment_callback is not None: hyperion_error_logger.info( diff --git a/app/core/core_endpoints/endpoints_core.py b/app/core/core_endpoints/endpoints_core.py index 2ee7963545..110eb903b2 100644 --- a/app/core/core_endpoints/endpoints_core.py +++ b/app/core/core_endpoints/endpoints_core.py @@ -14,7 +14,7 @@ is_user, is_user_super_admin, ) -from app.module import module_list +from app.module import get_module_list from app.types.module import CoreModule from app.utils.tools import is_group_id_valid, patch_identity_in_text @@ -209,7 +209,7 @@ async def get_module_visibility( """ return_module_visibilities = [] - for module in module_list: + for module in get_module_list(): allowed_group_ids = await cruds_core.get_allowed_groups_by_root( root=module.root, db=db, @@ -237,6 +237,7 @@ async def get_module_visibility( async def get_user_modules_visibility( db: AsyncSession = Depends(get_db), user: models_users.CoreUser = Depends(is_user()), + settings: Settings = Depends(get_settings), ): """ Get group user accessible root @@ -244,7 +245,11 @@ async def get_user_modules_visibility( **This endpoint is only usable by everyone** """ - return await cruds_core.get_modules_by_user(user=user, db=db) + modules = await cruds_core.get_modules_by_user(user=user, db=db) + if settings.RESTRICT_TO_MODULES: + return [module for module in modules if module in settings.RESTRICT_TO_MODULES] + + return modules @router.post( diff --git a/app/core/utils/config.py b/app/core/utils/config.py index 14f38325be..c4f9c6afc1 100644 --- a/app/core/utils/config.py +++ b/app/core/utils/config.py @@ -213,6 +213,10 @@ def settings_customise_sources( # If self registration is disabled, users will need to be invited by an administrator to be able to register ALLOW_SELF_REGISTRATION: bool = True + # Restrict to a list of module roots + # CoreModules can not be disabled + RESTRICT_TO_MODULES: list[str] | None = None + ############################ # PostgreSQL configuration # ############################ diff --git a/app/module.py b/app/module.py index 6dc4b04949..f240141042 100644 --- a/app/module.py +++ b/app/module.py @@ -2,37 +2,66 @@ import logging from pathlib import Path +from app.core.utils.config import Settings +from app.types.exceptions import InvalidModuleRootInDotenvError from app.types.module import CoreModule, Module hyperion_error_logger = logging.getLogger("hyperion.error") -module_list: list[Module] = [] -core_module_list: list[CoreModule] = [] -all_modules: list[CoreModule] = [] - -for endpoints_file in Path().glob("app/modules/*/endpoints_*.py"): - endpoint_module = importlib.import_module( - ".".join(endpoints_file.with_suffix("").parts), - ) - if hasattr(endpoint_module, "module"): - module: Module = endpoint_module.module - module_list.append(module) - else: - hyperion_error_logger.error( - f"Module {endpoints_file} does not declare a module. It won't be enabled.", +_module_list: list[Module] = [] +_core_module_list: list[CoreModule] = [] + + +def init_module_list(settings: Settings): + _module_list.clear() + _core_module_list.clear() + + module_list = [] + for endpoints_file in Path().glob("app/modules/*/endpoints_*.py"): + endpoint_module = importlib.import_module( + ".".join(endpoints_file.with_suffix("").parts), ) + if hasattr(endpoint_module, "module"): + module: Module = endpoint_module.module + module_list.append(module) + else: + hyperion_error_logger.error( + f"Module {endpoints_file} does not declare a module. It won't be enabled.", + ) + if settings.RESTRICT_TO_MODULES: + existing_module_roots = [module.root for module in module_list] + for root in settings.RESTRICT_TO_MODULES: + if root not in existing_module_roots: + raise InvalidModuleRootInDotenvError(root) + for module in module_list: + if ( + settings.RESTRICT_TO_MODULES + and module.root not in settings.RESTRICT_TO_MODULES + ): + continue + _module_list.append(module) -for endpoints_file in Path().glob("app/core/*/endpoints_*.py"): - endpoint_module = importlib.import_module( - ".".join(endpoints_file.with_suffix("").parts), - ) - if hasattr(endpoint_module, "core_module"): - core_module: CoreModule = endpoint_module.core_module - core_module_list.append(core_module) - else: - hyperion_error_logger.error( - f"Core module {endpoints_file} does not declare a core module. It won't be enabled.", + for endpoints_file in Path().glob("app/core/*/endpoints_*.py"): + endpoint_module = importlib.import_module( + ".".join(endpoints_file.with_suffix("").parts), ) + if hasattr(endpoint_module, "core_module"): + core_module: CoreModule = endpoint_module.core_module + _core_module_list.append(core_module) + else: + hyperion_error_logger.error( + f"Core module {endpoints_file} does not declare a core module. It won't be enabled.", + ) + + +def get_module_list() -> list[Module]: + return _module_list + + +def get_core_module_list() -> list[CoreModule]: + return _core_module_list + -all_modules = module_list + core_module_list +def get_all_modules() -> list[Module | CoreModule]: + return get_module_list() + get_core_module_list() diff --git a/app/types/exceptions.py b/app/types/exceptions.py index 0d5dee2ea8..52f8d623b6 100644 --- a/app/types/exceptions.py +++ b/app/types/exceptions.py @@ -32,6 +32,11 @@ def __init__(self): super().__init__("Google API is not configured in dotenv") +class InvalidModuleRootInDotenvError(Exception): + def __init__(self, root: str): + super().__init__(f"Module root {root} does not exist") + + class ContentHTTPException(HTTPException): """ A custom HTTPException allowing to return custom content. diff --git a/app/utils/screenshots/__init__.py b/app/utils/screenshots/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/utils/screenshots/ccc copie.jpeg b/app/utils/screenshots/ccc copie.jpeg new file mode 100644 index 0000000000..91888fb7e7 Binary files /dev/null and b/app/utils/screenshots/ccc copie.jpeg differ diff --git a/app/utils/screenshots/ccc.jpeg b/app/utils/screenshots/ccc.jpeg new file mode 100644 index 0000000000..f14d24eda3 Binary files /dev/null and b/app/utils/screenshots/ccc.jpeg differ diff --git a/app/utils/screenshots/cheerup.png b/app/utils/screenshots/cheerup.png new file mode 100644 index 0000000000..0d0e1d0db9 Binary files /dev/null and b/app/utils/screenshots/cheerup.png differ diff --git a/app/utils/screenshots/commuz copie.png b/app/utils/screenshots/commuz copie.png new file mode 100644 index 0000000000..8aa7de0a30 Binary files /dev/null and b/app/utils/screenshots/commuz copie.png differ diff --git a/app/utils/screenshots/commuz.png b/app/utils/screenshots/commuz.png new file mode 100644 index 0000000000..df67b61e13 Binary files /dev/null and b/app/utils/screenshots/commuz.png differ diff --git a/app/utils/screenshots/commuz_logo.png b/app/utils/screenshots/commuz_logo.png new file mode 100644 index 0000000000..7f39b1b584 Binary files /dev/null and b/app/utils/screenshots/commuz_logo.png differ diff --git a/app/utils/screenshots/factory_screenshots.py b/app/utils/screenshots/factory_screenshots.py new file mode 100644 index 0000000000..e120fe0cae --- /dev/null +++ b/app/utils/screenshots/factory_screenshots.py @@ -0,0 +1,148 @@ +import shutil +import uuid +from datetime import UTC, datetime, timedelta +from pathlib import Path + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.associations import cruds_associations +from app.core.associations.models_associations import CoreAssociation +from app.core.feed import cruds_feed, models_feed +from app.core.feed.types_feed import NewsStatus +from app.core.groups.groups_type import AccountType, GroupType +from app.core.schools.schools_type import SchoolType +from app.core.users import cruds_users +from app.core.users.models_users import CoreUser +from app.core.utils import security +from app.modules.advert import cruds_advert, models_advert +from app.modules.calendar import cruds_calendar, models_calendar +from app.modules.calendar.types_calendar import Decision + + +async def create_data(db: AsyncSession): + viewer_user_id = "31b17469-f7ca-4d89-9db5-3e568df69256" + user = CoreUser( + id=viewer_user_id, + password_hash=security.get_password_hash(password="password"), + firstname="Prénom", + nickname=None, + name="Nom", + email="prenom.nom@edu.em-lyon.fr", + floor=None, + phone=None, + promo=None, + school_id=SchoolType.base_school.value, + account_type=AccountType.student, + birthday=None, + created_on=datetime.now(tz=UTC), + ) + await cruds_users.create_user(db=db, user=user) + + commuz_association_id = uuid.uuid4() + cheerup_association_id = uuid.uuid4() + # await cruds_associations.create_association( + # db=db, + # association=CoreAssociation( + # id=uuid.uuid4(), + # name="Admin", + # group_id=GroupType.admin.value, + # ), + # ) + logo_folder = "data/associations/logos/" + await cruds_associations.create_association( + db=db, + association=CoreAssociation( + id=commuz_association_id, + name="Commuz'", + group_id=GroupType.admin.value, + ), + ) + shutil.copyfile( + "app/utils/screenshots/commuz_logo.png", + f"{logo_folder}{commuz_association_id}.png", + ) + await cruds_associations.create_association( + db=db, + association=CoreAssociation( + id=cheerup_association_id, + name="CheerUp", + group_id=GroupType.admin.value, + ), + ) + shutil.copyfile( + "app/utils/screenshots/cheerup.png", + f"{logo_folder}{cheerup_association_id}.png", + ) + + ouverture_des_castings_advert_id = uuid.uuid4() + ouverture_des_castings_advert = models_advert.Advert( + title="Ouverture des castings 🎭", + content="Vous voulez rejoindre la folle aventure de la Commuz' ?", + id=ouverture_des_castings_advert_id, + date=datetime.now(UTC) - timedelta(days=2), + advertiser_id=commuz_association_id, + post_to_feed=True, + ) + await cruds_advert.create_advert( + db=db, + db_advert=ouverture_des_castings_advert, + ) + advert_directory = "adverts" + Path(f"data/{advert_directory}").mkdir(parents=True, exist_ok=True) + shutil.copyfile( + "app/utils/screenshots/commuz.png", + f"data/{advert_directory}/{ouverture_des_castings_advert_id}.png", + ) + news = models_feed.News( + id=uuid.uuid4(), + title=ouverture_des_castings_advert.title, + start=ouverture_des_castings_advert.date, + end=None, + entity="Commuz'", + location=None, + action_start=None, + module="advert", + module_object_id=ouverture_des_castings_advert_id, + image_directory=advert_directory, + image_id=ouverture_des_castings_advert_id, + status=NewsStatus.PUBLISHED, + ) + await cruds_feed.create_news(news=news, db=db) + ccc_id = uuid.uuid4() + ccc = models_calendar.Event( + id=ccc_id, + name="Course contre le cancer", + association_id=cheerup_association_id, + applicant_id=viewer_user_id, + start=datetime.now(UTC), + end=datetime.now(UTC) + timedelta(days=3), + all_day=True, + location="emlyon", + description=None, + decision=Decision.approved, + recurrence_rule=None, + ticket_url_opening=None, + ticket_url=None, + ) + await cruds_calendar.add_event(db, ccc) + calendar_directory = "calendar" + Path(f"data/{calendar_directory}").mkdir(parents=True, exist_ok=True) + shutil.copyfile( + "app/utils/screenshots/ccc.jpeg", + f"data/{calendar_directory}/{ccc_id}.jpeg", + ) + news = models_feed.News( + id=uuid.uuid4(), + title=ccc.name, + start=ccc.start, + end=ccc.end, + entity="CheerUp", + location=None, + action_start=None, + module="calendar", + module_object_id=ccc_id, + image_directory=calendar_directory, + image_id=ccc_id, + status=NewsStatus.PUBLISHED, + ) + await cruds_feed.create_news(news=news, db=db) diff --git a/config.template.yaml b/config.template.yaml index 1e53b64bed..6943e494bf 100644 --- a/config.template.yaml +++ b/config.template.yaml @@ -54,6 +54,10 @@ SQLITE_DB: "app.db" # If True, will print all SQL queries in the console DATABASE_DEBUG: False +# Restrict to a list of module roots +# CoreModules can not be disabled +#RESTRICT_TO_MODULES: [] + ##################################### # SMTP configuration using starttls # ##################################### @@ -104,8 +108,9 @@ school: student_email_regex: '^[\w\-.]*@etu(-enise)?\.ec-lyon\.fr$' #staff_email_regex: #former_student_email_regex: + # Colors used for the application - primary_color: Color + primary_color: "#ED0000" # Regex for email account type validation # On registration, user whose email match these regex will be automatically assigned to the corresponding account type diff --git a/tests/test_factories.py b/tests/test_factories.py index 75b96fe810..dfd583913c 100644 --- a/tests/test_factories.py +++ b/tests/test_factories.py @@ -1,7 +1,7 @@ import pytest from fastapi.testclient import TestClient -from app.module import all_modules +from app.module import get_all_modules from tests.commons import get_TestingSessionLocal @@ -9,7 +9,7 @@ async def test_factories(client: TestClient) -> None: async with get_TestingSessionLocal()() as db: factories = [ - module.factory for module in all_modules if module.factory is not None + module.factory for module in get_all_modules() if module.factory is not None ] for factory in factories: assert not await factory.should_run(