add(templates): base models and dto for girasol backend
Some checks failed
Deploy Girasol / deploy (push) Has been cancelled

This commit is contained in:
Julien Aldon
2026-04-23 18:17:31 +02:00
parent e91f140335
commit 89c20338dd
18 changed files with 858 additions and 0 deletions

176
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,176 @@
# Created by https://www.toptal.com/developers/gitignore/api/python
# Edit at https://www.toptal.com/developers/gitignore?templates=python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python

6
backend/README.md Normal file
View File

@@ -0,0 +1,6 @@
# FastAPI Backend
```sh
hatch shell
fastapi dev
```

72
backend/pyproject.toml Normal file
View File

@@ -0,0 +1,72 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "backend"
dynamic = ["version"]
description = ''
readme = "README.md"
requires-python = ">=3.11"
license = "MIT"
keywords = []
authors = [
{ name = "Julien Aldon", email = "julien.aldon@wanadoo.fr" },
]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = [
"fastapi[standard]",
"sqlmodel",
"psycopg2-binary",
"PyJWT",
"cryptography",
"pytest",
"pytest-cov",
"pytest-mock",
"pylint",
]
[project.urls]
Documentation = ""
Issues = ""
Source = ""
[tool.hatch.version]
path = "src/__about__.py"
[tool.hatch.envs.types]
extra-dependencies = [
"mypy>=1.0.0",
]
[tool.hatch.envs.types.scripts]
check = "mypy --install-types --non-interactive {args:src tests}"
[tool.coverage.run]
source_pkgs = ["backend", "tests"]
branch = true
parallel = true
omit = [
"src/__about__.py",
]
[tool.coverage.paths]
backend = ["src", "*/backend/src/"]
tests = ["tests", "*/backend/tests"]
[tool.coverage.report]
exclude_lines = [
"no cov",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
[tool.hatch.build.targets.wheel]
packages = ["src"]
include = ["src/**/*.py"]

6
backend/src/__about__.py Normal file
View File

@@ -0,0 +1,6 @@
"""About
"""
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
#
# SPDX-License-Identifier: MIT
__version__ = "0.0.1"

3
backend/src/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
#
# SPDX-License-Identifier: MIT

95
backend/src/events/dto.py Normal file
View File

@@ -0,0 +1,95 @@
"""Event module DTO
"""
import datetime
import uuid
from typing import List, Optional
from sqlmodel import Field, SQLModel
from src.events.models import BaseEdition, BaseSeries, BaseTag
from src.files.dto import ReadFile
class CreateSeries(BaseSeries):
"""CreateSeries DTO
"""
logotype_id: uuid.UUID = Field()
class UpdateSeries(SQLModel):
"""UpdateSeries DTO
"""
title: Optional[str] = None
color_light: Optional[str] = None
color_dark: Optional[str] = None
color_strong: Optional[str] = None
class ReadSeriesLight(BaseSeries):
"""Lighter version of ReadSeries for nested usage.
"""
id: uuid.UUID
logotype: ReadFile
class ReadSeries(BaseSeries):
"""ReadSeries DTO
"""
id: uuid.UUID
logotype: ReadFile
editions: List['ReadEdition'] = Field(default_factory=list)
class CreateEdition(BaseEdition):
"""CreateEdition DTO
"""
flyer_id: uuid.UUID = Field()
hero_id: uuid.UUID = Field()
series_id: uuid.UUID = Field()
class UpdateEdition(SQLModel):
"""UpdateEdition DTO
"""
title: Optional[str] = None
long_description: Optional[dict] = None
short_description: Optional[str] = None
start_date: Optional[datetime.datetime] = None
end_date: Optional[datetime.datetime] = None
contact_information: Optional[dict] = None
class ReadEdition(BaseEdition):
"""ReadEdition DTO
"""
id: uuid.UUID
sponsors: List['ReadSponsor'] = Field(default_factory=list)
flyer: ReadFile
hero: ReadFile
series: ReadSeriesLight
tags: List['ReadTagLight'] = Field(default_factory=list)
gallery: List[ReadFile] = Field(default_factory=list)
class CreateTag(BaseTag):
"""CreateTag DTO
"""
class UpdateTag(SQLModel):
"""UpdateTag DTO
"""
title: Optional[str] = None
color: Optional[str] = None
class ReadTagLight(BaseTag):
"""Lighter version of ReadTag for nested usage.
"""
id: uuid.UUID
class ReadTag(BaseTag):
"""ReadTag DTO
"""
id: uuid.UUID
editions: List['ReadEdition'] = Field(default_factory=list)

View File

@@ -0,0 +1,145 @@
"""Event module models
"""
import datetime
import uuid
from sqlalchemy.dialects.postgresql import JSONB
from sqlmodel import Field, Relationship, SQLModel
class BaseSeries(SQLModel):
"""Base model for a series.
Attributes:
title (str): Name of the series.
color_light (str): Light theme color.
color_dark (str): Dark theme color.
color_strong (str): Accent / Primary color.
"""
title: str
color_light: str
color_dark: str
color_strong: str
class Series(BaseSeries, table=True):
"""Database model representing a series of editions.
A series of events under the same branding
Attributes:
id (uuid.UUID): unique identifier of the series.
logotype_id (uuid.UUID): Reference to the logotype file.
editions (list['Edition']): List of editions belonging to this series.
"""
id: uuid.UUID | None = Field(
default=None, default_factory=uuid.uuid4, primary_key=True)
logotype_id: uuid.UUID = Field(foreign_key="file.id")
editions: list['Edition'] = Relationship(back_populates='series')
class BaseEdition(SQLModel):
"""Base model for an edition
Attributes:
title (str): Name of the edition.
long_description (dict): Description block of the edition.
short_description (str): Description of the edition for SEO.
start_date (datetime.datetime): Start date of the edition.
end_date (datetime.datetime): End date of the edition.
contact_information (dict): Contact information block for contact page.
"""
title: str
long_description: dict = Field(sa_type=JSONB, nullable=False)
short_description: str
start_date: datetime.datetime
end_date: datetime.datetime
contact_information: dict = Field(sa_type=JSONB, nullable=False)
class EditionTagLink(SQLModel, table=True):
"""Association table linking editions and tags
Represents a many-to-many relationship between editions and tags.
"""
edition_id: uuid.UUID = Field(foreign_key="edition.id", primary_key=True)
tag_id: uuid.UUID = Field(foreign_key="tag.id", primary_key=True)
class EditionFileLink(SQLModel, table=True):
"""Association table linking editions and files
Represents a many-to-many relationship between editions and files.
"""
edition_id: uuid.UUID = Field(foreign_key="edition.id", primary_key=True)
file_id: uuid.UUID = Field(foreign_key="file.id", primary_key=True)
class EditionSponsorLink(SQLModel, table=True):
"""Association table linking editions and sponsors.
Represents a many-to-many relationship between editions and sponsors.
"""
edition_id: uuid.UUID = Field(foreign_key="edition.id", primary_key=True)
sponsor_id: uuid.UUID = Field(foreign_key="sponsor.id", primary_key=True)
class Edition(BaseEdition, table=True):
"""Database model representing an A single iteration of an event.
An edition will have it's own page to display its informations.
Attributes:
id (uuid.UUID): unique identifier of the edition.
series_id (uuid.UUID): Reference to edition's series
flyer_id (uuid.UUID): Reference to the flyer file.
Image used as flyer under herobanner in edition Page.
hero_id (uuid.UUID): Reference to the hero file.
Image used as herobanner for edition Page.
sponsors (List['Sponsor']): List of sponsors belonging to this edition.
series (Series): Series Object reference.
tags (List('Tag')): List of tags associated to this edition.
tags (List('File')): List of Images associated to this edition.
"""
id: uuid.UUID | None = Field(
default=None, default_factory=uuid.uuid4, primary_key=True)
series_id: uuid.UUID = Field(foreign_key="series.id")
flyer_id: uuid.UUID = Field(foreign_key="file.id")
hero_id: uuid.UUID = Field(foreign_key="file.id")
sponsors: list['Sponsor'] = Relationship(
cascade_delete=True,
link_model=EditionSponsorLink,
)
series: Series = Relationship(back_populates="editions")
tags: list['Tag'] = Relationship(
link_model=EditionTagLink
)
gallery: list['File'] = Relationship(
link_model=EditionFileLink
)
class BaseTag(SQLModel):
"""Base model for a Tag
Attributes:
title (str): Name of the tag.
color (str): Color of the tag.
"""
title: str
color: str
class Tag(BaseTag, table=True):
"""Database model representing a Tag
A tag is a way to group events with a mutual interests.
Attributes:
id (uudi.UUID): Unique identifier of a tag
"""
id: uuid.UUID | None = Field(
default=None, default_factory=uuid.uuid4, primary_key=True)
editions: list['Edition'] = Relationship(
link_model=EditionTagLink
)

27
backend/src/files/dto.py Normal file
View File

@@ -0,0 +1,27 @@
"""Files module DTO
"""
import uuid
from typing import Optional
from sqlmodel import SQLModel
from src.files.models import BaseFile
class CreateFile(BaseFile):
"""CreateFile DTO
"""
class UpdateFile(SQLModel):
"""UpdateFile DTO
"""
name: Optional[str] = None
type: Optional[str] = None
class ReadFile(BaseFile):
"""ReadFile DTO
"""
id: uuid.UUID
path_full: str
path_preview: str | None = None

View File

@@ -0,0 +1,31 @@
"""File module models
"""
import uuid
from sqlmodel import Field, SQLModel
class BaseFile(SQLModel):
"""Base model for Files
Attributes:
name (str): Name of the file (used in alt).
type (str): Type of the file.
"""
name: str
type: str
class File(BaseFile, table=True):
"""Database model representing a File
A file is a stored file through the media center, mostly images.
Attributes:
path_full (str): Path toward the stored file.
path_preview (str | None): Path toward a preview.
"""
id: uuid.UUID | None = Field(
default=None, default_factory=uuid.uuid4, primary_key=True)
path_full: str
path_preview: str | None = None

30
backend/src/links/dto.py Normal file
View File

@@ -0,0 +1,30 @@
"""Links module DTO
"""
import uuid
from typing import Optional
from sqlmodel import SQLModel
from src.events.dto import ReadEdition
from src.links.models import BaseLink
class CreateLink(BaseLink):
"""CreateLink DTO
"""
edition_id: uuid.UUID
class UpdateLink(SQLModel):
"""UpdateLink DTO
"""
title: Optional[str] = None
icon: Optional[str] = None
url: Optional[str] = None
order: Optional[int] = None
class ReadLink(BaseLink):
"""ReadLink DTO
"""
id: uuid.UUID
edition: Optional[ReadEdition] = None

View File

@@ -0,0 +1,36 @@
"""Link module models
"""
import uuid
from sqlmodel import Field, SQLModel
class BaseLink(SQLModel):
"""Base model for a Link
Attributes:
title (str): Name of the link.
icon (str): Icon to display this link (using icon library).
url (str): Url of the link.
order (int): Order of occurence in page.
"""
title: str
icon: str
url: str
order: int
class Link(BaseLink, table=True):
"""Database model representing a Link
A link featured on an edition's page or in the website's header.
Attributes:
id (uudi.UUID): Unique identifier for a link.
edition_id (uuid.UUID | None): Unique identifier of the
edition associated to this link (*optional*)
"""
id: uuid.UUID | None = Field(
default=None, default_factory=uuid.uuid4, primary_key=True)
edition_id: uuid.UUID | None = Field(
default=None, foreign_key="edition.id")

3
backend/src/main.py Normal file
View File

@@ -0,0 +1,3 @@
from fastapi import FastAPI
app = FastAPI()

34
backend/src/news/dto.py Normal file
View File

@@ -0,0 +1,34 @@
"""News module DTO
"""
import datetime
import uuid
from typing import Optional
from sqlmodel import SQLModel
from src.files.dto import ReadFile
from src.news.models import BaseNews
class CreateNews(BaseNews):
"""CreateNews DTO
"""
hero_id: uuid.UUID
class UpdateNews(SQLModel):
"""UpdateNews DTO
"""
title: Optional[str] = None
subtitle: Optional[str] = None
long_description: Optional[dict] = None
short_description: Optional[str] = None
carousel: Optional[bool] = None
banner: Optional[bool] = None
expiry_date: Optional[datetime.datetime] = None
class ReadNews(BaseNews):
"""ReadNews DTO
"""
id: uuid.UUID
hero: Optional[ReadFile] = None

View File

@@ -0,0 +1,32 @@
"""News module models
"""
import datetime
import uuid
from sqlalchemy.dialects.postgresql import JSONB
from sqlmodel import Field, SQLModel
class BaseNews(SQLModel):
"""Base model for a News
Attributes:
"""
title: str
subtitle: str
long_description: dict = Field(sa_type=JSONB, nullable=False)
short_description: str
carousel: bool
banner: bool
expiry_date: datetime.datetime
class News(BaseNews, table=True):
"""Database model representing a News
A plain blog post unrelated to a specific event,
appears in the homepage carousel, as a site-wide banner or both
"""
id: uuid.UUID | None = Field(
default=None, default_factory=uuid.uuid4, primary_key=True)
hero_id: uuid.UUID = Field(foreign_key="file.id")

View File

@@ -0,0 +1,50 @@
"""Sitewide module DTO
"""
import uuid
from typing import Optional
from sqlmodel import SQLModel
from src.files.dto import ReadFile
from src.sitewide.models import (BaseEmbeddedBlock, BaseSite,
EmbeddedBlockLayout, EmbeddedBlockLocation)
class CreateSite(BaseSite):
"""CreateSite DTO
"""
image_id: uuid.UUID
class UpdateSite(SQLModel):
"""UpdateSite DTO
"""
legal: Optional[dict] = None
about: Optional[dict] = None
short_description: Optional[str] = None
class ReadSite(BaseSite):
"""ReadSite DTO
"""
id: uuid.UUID
class CreateEnbeddedBlock(BaseEmbeddedBlock):
"""CreateEnbeddedBlock DTO
"""
class UpdateEnbeddedBlock(SQLModel):
"""UpdateEnbeddedBlock DTO
"""
title: Optional[str] = None
layout: Optional[EmbeddedBlockLayout] = None
location: Optional[EmbeddedBlockLocation] = None
description: Optional[dict] = None
class ReadEnbeddedBlock(BaseEmbeddedBlock):
"""ReadEnbeddedBlock DTO
"""
id: uuid.UUID
image: ReadFile

View File

@@ -0,0 +1,55 @@
"""Sitewide module models
"""
import uuid
from enum import StrEnum
from sqlalchemy.dialects.postgresql import JSONB
from sqlmodel import Field, SQLModel
class EmbeddedBlockLayout(StrEnum):
"""Relative layout of an embedded block's image and description
"""
LEFT = 'left'
RIGHT = 'right'
CENTER = 'center'
class EmbeddedBlockLocation(StrEnum):
"""The location where an embedded block should appear
"""
HOME = 'home'
CONTACT = 'contact'
class BaseSite(SQLModel):
"""BaseSite
"""
legal: dict = Field(sa_type=JSONB, nullable=False)
about: dict = Field(sa_type=JSONB, nullable=False)
short_description: str
class Site(BaseSite, table=True):
"""Database model representing the Site-wide configuration
"""
id: uuid.UUID | None = Field(
default=None, default_factory=uuid.uuid4, primary_key=True)
class BaseEmbeddedBlock(SQLModel):
"""BaseEmbeddedBlock
"""
title: str
layout: EmbeddedBlockLayout
location: EmbeddedBlockLocation
description: dict = Field(sa_type=JSONB, nullable=False)
class EmbeddedBlock(BaseEmbeddedBlock, table=True):
"""A paragraph displayed on the home page or contact page
with information about Girasol
"""
id: uuid.UUID | None = Field(
default=None, default_factory=uuid.uuid4, primary_key=True)
image_id: uuid.UUID = Field(foreign_key="file.id")

View File

@@ -0,0 +1,29 @@
"""Sponsor module DTO
"""
import uuid
from typing import List, Optional
from sqlmodel import Field, SQLModel
from src.events.dto import ReadEdition
from src.sponsors.models import BaseSponsor
class CreateSponsor(BaseSponsor):
"""CreateSponsor DTO
"""
image_id: uuid.UUID
class UpdateSponsor(SQLModel):
"""UpdateSponsor DTO
"""
title: Optional[str] = None
sitewide: Optional[bool] = None
url: Optional[str] = None
class ReadSponsor(BaseSponsor):
"""ReadSponsor DTO
"""
id: uuid.UUID
editions: List['ReadEdition'] = Field(default_factory=list)

View File

@@ -0,0 +1,28 @@
"""Sponsor module model
"""
import uuid
from sqlmodel import Field, Relationship, SQLModel
from src.sponsors.models import EditionSponsorLink
class BaseSponsor(SQLModel):
"""Base Sponsor
"""
title: str
sitewide: bool
url: str
class Sponsor(BaseSponsor, table=True):
"""Database model representing a Sponsor
A sponsor for Girasol's activities
"""
id: uuid.UUID | None = Field(
default=None, default_factory=uuid.uuid4, primary_key=True)
image_id: uuid.UUID = Field(foreign_key="file.id")
editions: list['Edition'] = Relationship(
cascade_delete=True,
link_model=EditionSponsorLink,
)