Compare commits

58 Commits

Author SHA1 Message Date
5e413b11e0 add permission check for form productor and product 2026-03-04 23:36:17 +01:00
Julien Aldon
6679107b13 downgrade python version in tests
All checks were successful
Deploy Amap / deploy (push) Successful in 1m47s
2026-03-03 14:34:00 +01:00
Julien Aldon
20eba7f183 remove debug in router
Some checks failed
Deploy Amap / deploy (push) Failing after 11s
2026-03-03 11:37:40 +01:00
Julien Aldon
c6d75831c9 add debug for tests
Some checks failed
Deploy Amap / deploy (push) Failing after 11s
2026-03-03 11:37:23 +01:00
Julien Aldon
b2e2d02818 add debug for tests
Some checks failed
Deploy Amap / deploy (push) Failing after 10s
2026-03-03 11:36:22 +01:00
Julien Aldon
8cb7893aff add debug for tests
Some checks failed
Deploy Amap / deploy (push) Failing after 10s
2026-03-03 11:34:41 +01:00
Julien Aldon
015e09a980 add debug for tests
Some checks failed
Deploy Amap / deploy (push) Failing after 10s
2026-03-03 11:31:16 +01:00
Julien Aldon
a70ab5d3cb add debug for tests
Some checks failed
Deploy Amap / deploy (push) Failing after 10s
2026-03-03 11:29:57 +01:00
Julien Aldon
9d5dbd80cc add debug for tests
Some checks failed
Deploy Amap / deploy (push) Failing after 10s
2026-03-03 11:27:01 +01:00
Julien Aldon
1c6e810ec1 fix python test version
Some checks failed
Deploy Amap / deploy (push) Failing after 1m30s
2026-03-03 11:15:39 +01:00
Julien Aldon
8352097ffb fix pylint errors
Some checks failed
Deploy Amap / deploy (push) Failing after 16s
2026-03-03 11:08:08 +01:00
Julien Aldon
0e48d1bbaa fix test format 2026-03-02 16:33:52 +01:00
Julien Aldon
5dd9e19877 add partial contract tests
Some checks failed
Deploy Amap / deploy (push) Failing after 44s
2026-03-02 11:45:05 +01:00
Julien Aldon
4a4c1225dc fix python version for tests
All checks were successful
Deploy Amap / deploy (push) Successful in 7m25s
2026-02-27 13:45:33 +01:00
Julien Aldon
9f57b11fcf fix version dependencies
Some checks failed
Deploy Amap / deploy (push) Failing after 31s
2026-02-27 13:34:55 +01:00
Julien Aldon
e303e0723e add forms, shipments tests
Some checks failed
Deploy Amap / deploy (push) Failing after 13s
2026-02-27 12:29:07 +01:00
Julien Aldon
d28640711c add forms, shipments tests
Some checks failed
Deploy Amap / deploy (push) Failing after 52s
2026-02-27 12:21:50 +01:00
Julien Aldon
61cbbf0366 add tests forn forms, products, productors
All checks were successful
Deploy Amap / deploy (push) Successful in 3m45s
2026-02-25 16:39:12 +01:00
Julien Aldon
cfb8d435a8 Merge branch 'main' of gitea.aldon.fr:Mop/amap
All checks were successful
Deploy Amap / deploy (push) Successful in 35s
2026-02-23 15:38:45 +01:00
Julien Aldon
124b0700da add visible field to form 2026-02-23 15:38:29 +01:00
da22f24198 fix layout
All checks were successful
Deploy Amap / deploy (push) Successful in 9s
2026-02-20 18:51:45 +01:00
f4bb71a296 fix layout 2026-02-20 18:51:11 +01:00
71a62e6d19 fix type max
All checks were successful
Deploy Amap / deploy (push) Successful in 31s
2026-02-20 18:16:10 +01:00
41552a889f fix cheque validation
Some checks failed
Deploy Amap / deploy (push) Failing after 15s
2026-02-20 18:13:47 +01:00
Julien Aldon
85a70da07d add max payment method for cheque
All checks were successful
Deploy Amap / deploy (push) Successful in 34s
2026-02-20 17:36:32 +01:00
Julien Aldon
72f8005fbd fix recurent and layout none
All checks were successful
Deploy Amap / deploy (push) Successful in 9s
2026-02-20 16:26:19 +01:00
Julien Aldon
05480b44df add debug
All checks were successful
Deploy Amap / deploy (push) Successful in 9s
2026-02-20 16:19:43 +01:00
Julien Aldon
f009674b13 fix contract dto
All checks were successful
Deploy Amap / deploy (push) Successful in 9s
2026-02-20 16:02:51 +01:00
Julien Aldon
2177a77fcf fix eslint
All checks were successful
Deploy Amap / deploy (push) Successful in 34s
2026-02-20 15:54:47 +01:00
Julien Aldon
b662a6a1f0 add base template for manual fill
Some checks failed
Deploy Amap / deploy (push) Failing after 15s
2026-02-20 15:42:15 +01:00
Julien Aldon
12be0e5650 remove doublons
All checks were successful
Deploy Amap / deploy (push) Successful in 9s
2026-02-20 13:58:30 +01:00
Julien Aldon
ef5d9e8455 add default cheque quantity
All checks were successful
Deploy Amap / deploy (push) Successful in 29s
2026-02-20 13:49:10 +01:00
Julien Aldon
6dd5ade890 remove debug
All checks were successful
Deploy Amap / deploy (push) Successful in 9s
2026-02-20 13:37:03 +01:00
Julien Aldon
34b2436ca0 fix contract submission
All checks were successful
Deploy Amap / deploy (push) Successful in 33s
2026-02-20 12:05:41 +01:00
Julien Aldon
af4941c1b9 fix contract generation
All checks were successful
Deploy Amap / deploy (push) Successful in 9s
2026-02-20 11:17:20 +01:00
Julien Aldon
7a94f1c96a fix app crash
All checks were successful
Deploy Amap / deploy (push) Successful in 32s
2026-02-20 10:33:00 +01:00
e37fae439f fix role route
All checks were successful
Deploy Amap / deploy (push) Successful in 9s
2026-02-20 02:07:48 +01:00
6406009038 add checks
All checks were successful
Deploy Amap / deploy (push) Successful in 29s
2026-02-20 02:05:43 +01:00
b05276eeeb remove trailing
All checks were successful
Deploy Amap / deploy (push) Successful in 8s
2026-02-20 01:59:33 +01:00
c67e59d5db fix dockerfile
All checks were successful
Deploy Amap / deploy (push) Successful in 8s
2026-02-20 01:48:34 +01:00
754922cbf0 fix dockerfile
All checks were successful
Deploy Amap / deploy (push) Successful in 8s
2026-02-20 01:43:07 +01:00
9a7217a54e fix prefix
All checks were successful
Deploy Amap / deploy (push) Successful in 8s
2026-02-20 01:38:17 +01:00
0ff2d4bb01 fix dockerfile
All checks were successful
Deploy Amap / deploy (push) Successful in 9s
2026-02-20 01:36:22 +01:00
26f087ea8b fix typo
All checks were successful
Deploy Amap / deploy (push) Successful in 9s
2026-02-20 01:13:48 +01:00
a530e71103 add dockerfile
All checks were successful
Deploy Amap / deploy (push) Successful in 8s
2026-02-20 01:12:23 +01:00
53490c86f0 fix path
All checks were successful
Deploy Amap / deploy (push) Successful in 7s
2026-02-20 01:09:12 +01:00
63934e6287 fix nginx
All checks were successful
Deploy Amap / deploy (push) Successful in 29s
2026-02-20 00:52:57 +01:00
eb19efe225 fix nginx
All checks were successful
Deploy Amap / deploy (push) Successful in 29s
2026-02-20 00:48:39 +01:00
4fbc1e6621 fix nginx
All checks were successful
Deploy Amap / deploy (push) Successful in 9s
2026-02-20 00:40:42 +01:00
e478b26943 fix nginx conf
All checks were successful
Deploy Amap / deploy (push) Successful in 29s
2026-02-20 00:30:10 +01:00
63ea2ff806 fix nginx internal rooting
All checks were successful
Deploy Amap / deploy (push) Successful in 29s
2026-02-20 00:14:51 +01:00
b52ac35593 fix dockercompose
All checks were successful
Deploy Amap / deploy (push) Successful in 8s
2026-02-20 00:11:37 +01:00
a16c940452 fix alembic first migration
Some checks failed
Deploy Amap / deploy (push) Failing after 5s
2026-02-20 00:10:36 +01:00
f6101a251a fix app dockerfile
Some checks failed
Deploy Amap / deploy (push) Failing after 8s
2026-02-19 23:55:39 +01:00
a854fc028e fix generate contract path and add alembic to dependencies
Some checks failed
Deploy Amap / deploy (push) Failing after 22s
2026-02-19 23:50:00 +01:00
242e29c8a6 add workflow
Some checks failed
Deploy Amap / deploy (push) Has been cancelled
2026-02-19 23:38:26 +01:00
Julien Aldon
7574626e52 add docker compose 2026-02-19 17:34:15 +01:00
Julien Aldon
1bd0583c70 fix auth login / logout / refresh token 2026-02-19 16:20:45 +01:00
125 changed files with 5744 additions and 624 deletions

View File

@@ -0,0 +1,25 @@
name: Deploy Amap
on:
push:
branches:
- main
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Test backend
uses: actions/setup-python@v6
with:
python-version: "3.12"
- run: |
python -m pip install --upgrade pip
pip install -r backend/requirements.txt
pytest -sv
- name: Build & deploy
run: |
docker compose -f docker-compose.yaml up -d --build
docker compose -f docker-compose.yaml exec backend alembic upgrade head

View File

@@ -2,24 +2,14 @@
## backend\src\contracts\contracts.py
- Send contract to referer
- Extract recap
- Extract all contracts
- store total price
## Link products to a form
## Wording
- all translations
## Draft / Publish form
- By default form is in draft mode
- Validate a form (button)
- check if productor
- check if shipments
- check products
- Publish
## Footer
### Legal
@@ -28,14 +18,9 @@
### Contact
## Migrations
## Pagination
- use alembic for migration management
## Confirmation modal on suppression
### Show on cascade deletion
## Filter forms in home view
## Only show current season (if multiple form, only show the one with latest start date)
## Update contract after register
## Default filter
## Update contract after (without registration)

View File

@@ -0,0 +1,26 @@
default_language_version:
python: python3.13
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: trailing-whitespace
- id: check-ast
- id: check-builtin-literals
- id: check-docstring-first
- id: check-yaml
- id: check-toml
- id: mixed-line-ending
- id: end-of-file-fixer
- repo: local
hooks:
- id: check-pylint
name: check-pylint
entry: pylint -d R0801,R0903,W0511,W0603,C0103,R0902
language: system
types: [python]
pass_filenames: false
args:
- backend

13
backend/Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
FROM python:3.12
WORKDIR /code
RUN apt update && apt install -y weasyprint
COPY ./backend/requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY ./backend /code
CMD ["fastapi", "run", "src/main.py", "--port", "8000", "--forwarded-allow-ips", "*", "--proxy-headers"]

View File

@@ -18,6 +18,29 @@ hatch shell
fastapi dev src/main.py
```
### Migration
This repository use `alembic` for migrations
#### Create migration
```console
alembic revision --autogenerate -m "message"
```
#### Apply migration
```console
alembic upgrade head
```
## Tests
```
hatch run pytest
hatch run pytest --cov=src -vv
```
## Autoformat
```console
find -type f -name '*.py' ! -path 'alembic/*' -exec autopep8 --in-place --aggressive --aggressive '{}' \;
pylint -d R0801,R0903,W0511,W0603,C0103,R0902 .
```
## License
`backend` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.

147
backend/alembic.ini Normal file
View File

@@ -0,0 +1,147 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/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
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the tzdata library which can be 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 <script_location>/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 "path_separator"
# below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
# path_separator; This indicates what character is used to split lists of file
# paths, including version_locations and prepend_sys_path within configparser
# files such as alembic.ini.
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os
# 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
# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
[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 module runner, against the "ruff" module
# hooks = ruff
# ruff.type = module
# ruff.module = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Alternatively, use the exec runner to execute a binary found on your PATH
# hooks = ruff
# ruff.type = exec
# ruff.executable = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
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

1
backend/alembic/README Normal file
View File

@@ -0,0 +1 @@
Generic single-database configuration.

82
backend/alembic/env.py Normal file
View File

@@ -0,0 +1,82 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from sqlmodel import SQLModel
from src.settings import settings
from src.models import *
# 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
target_metadata = SQLModel.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
config.set_main_option(
"sqlalchemy.url", f'postgresql://{settings.db_user}:{settings.db_pass}@{settings.db_host}:5432/{settings.db_name}')
# ... 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()

View File

@@ -0,0 +1,29 @@
"""${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 ""}
import sqlmodel.sql.sqltypes
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[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:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,38 @@
"""message
Revision ID: 7854064278ce
Revises: c0b1073a8394
Create Date: 2026-02-20 17:17:25.739406
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
# revision identifiers, used by Alembic.
revision: str = '7854064278ce'
down_revision: Union[str, Sequence[str], None] = 'c0b1073a8394'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
'paymentmethod',
sa.Column(
'max',
sa.Integer(),
nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('paymentmethod', 'max')
# ### end Alembic commands ###

View File

@@ -0,0 +1,159 @@
"""Initial repository
Revision ID: c0b1073a8394
Revises:
Create Date: 2026-02-20 00:09:35.920486
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
# revision identifiers, used by Alembic.
revision: str = 'c0b1073a8394'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
'contracttype',
sa.Column(
'id',
sa.Integer(),
nullable=False),
sa.Column(
'name',
sqlmodel.sql.sqltypes.AutoString(),
nullable=False),
sa.PrimaryKeyConstraint('id'))
op.create_table(
'productor', sa.Column(
'name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column(
'address', sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column(
'type', sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column(
'id', sa.Integer(), nullable=False), sa.PrimaryKeyConstraint('id'))
op.create_table('template',
sa.Column('id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table(
'user', sa.Column(
'name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column(
'email', sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column(
'id', sa.Integer(), nullable=False), sa.PrimaryKeyConstraint('id'))
op.create_table('form',
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('productor_id', sa.Integer(), nullable=True),
sa.Column('referer_id', sa.Integer(), nullable=True),
sa.Column('season', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('start', sa.Date(), nullable=False),
sa.Column('end', sa.Date(), nullable=False),
sa.Column('minimum_shipment_value', sa.Float(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['productor_id'], ['productor.id'], ),
sa.ForeignKeyConstraint(['referer_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('paymentmethod',
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('details', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('productor_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['productor_id'], ['productor.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('product',
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('unit', sa.Enum('GRAMS', 'KILO', 'PIECE', name='unit'), nullable=False),
sa.Column('price', sa.Float(), nullable=True),
sa.Column('price_kg', sa.Float(), nullable=True),
sa.Column('quantity', sa.Float(), nullable=True),
sa.Column('quantity_unit', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('type', sa.Enum('OCCASIONAL', 'RECCURENT', name='producttype'), nullable=False),
sa.Column('productor_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['productor_id'], ['productor.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table(
'usercontracttypelink', sa.Column(
'user_id', sa.Integer(), nullable=False), sa.Column(
'contract_type_id', sa.Integer(), nullable=False), sa.ForeignKeyConstraint(
['contract_type_id'], ['contracttype.id'], ), sa.ForeignKeyConstraint(
['user_id'], ['user.id'], ), sa.PrimaryKeyConstraint(
'user_id', 'contract_type_id'))
op.create_table('contract',
sa.Column('firstname', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('lastname', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('phone', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('payment_method', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('cheque_quantity', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('form_id', sa.Integer(), nullable=False),
sa.Column('file', sa.LargeBinary(), nullable=True),
sa.Column('total_price', sa.Float(), nullable=True),
sa.ForeignKeyConstraint(['form_id'], ['form.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('shipment',
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('date', sa.Date(), nullable=False),
sa.Column('form_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['form_id'], ['form.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('cheque',
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('value', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('contract_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['contract_id'], ['contract.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('contractproduct',
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('shipment_id', sa.Integer(), nullable=True),
sa.Column('quantity', sa.Float(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('contract_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['contract_id'], ['contract.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['shipment_id'], ['shipment.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('shipmentproductlink',
sa.Column('shipment_id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.ForeignKeyConstraint(['shipment_id'], ['shipment.id'], ),
sa.PrimaryKeyConstraint('shipment_id', 'product_id')
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('shipmentproductlink')
op.drop_table('contractproduct')
op.drop_table('cheque')
op.drop_table('shipment')
op.drop_table('contract')
op.drop_table('usercontracttypelink')
op.drop_table('product')
op.drop_table('paymentmethod')
op.drop_table('form')
op.drop_table('user')
op.drop_table('template')
op.drop_table('productor')
op.drop_table('contracttype')
# ### end Alembic commands ###

View File

@@ -0,0 +1,40 @@
"""message
Revision ID: e777ed5729ce
Revises: 7854064278ce
Create Date: 2026-02-23 13:53:09.999893
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
# revision identifiers, used by Alembic.
revision: str = 'e777ed5729ce'
down_revision: Union[str, Sequence[str], None] = '7854064278ce'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
'form',
sa.Column(
'visible',
sa.Boolean(),
nullable=False,
default=False,
server_default="False"))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('form', 'visible')
# ### end Alembic commands ###

View File

@@ -29,7 +29,14 @@ dependencies = [
"cryptography",
"requests",
"weasyprint",
"odfdo"
"odfdo",
"alembic",
"pytest",
"pytest-cov",
"pytest-mock",
"autopep8",
"prek",
"pylint",
]
[project.urls]

84
backend/requirements.txt Normal file
View File

@@ -0,0 +1,84 @@
alembic==1.18.4
annotated-doc==0.0.4
annotated-types==0.7.0
anyio==4.12.1
astroid==4.0.4
autopep8==2.3.2
brotli==1.2.0
certifi==2026.2.25
cffi==2.0.0
charset-normalizer==3.4.4
click==8.3.1
coverage==7.13.4
cryptography==46.0.5
cssselect2==0.9.0
dill==0.4.1
dnspython==2.8.0
email-validator==2.3.0
fastapi==0.135.1
fastapi-cli==0.0.24
fastapi-cloud-cli==0.14.0
fastar==0.8.0
fonttools==4.61.1
greenlet==3.3.2
h11==0.16.0
httpcore==1.0.9
httptools==0.7.1
httpx==0.28.1
idna==3.11
iniconfig==2.3.0
isort==8.0.1
Jinja2==3.1.6
lxml==6.0.2
Mako==1.3.10
markdown-it-py==4.0.0
MarkupSafe==3.0.3
mccabe==0.7.0
mdurl==0.1.2
odfdo==3.21.0
packaging==26.0
pillow==12.1.1
platformdirs==4.9.2
pluggy==1.6.0
prek==0.3.4
psycopg2-binary==2.9.11
pycodestyle==2.14.0
pycparser==3.0
pydantic==2.12.5
pydantic-extra-types==2.11.0
pydantic-settings==2.13.1
pydantic_core==2.41.5
pydyf==0.12.1
Pygments==2.19.2
PyJWT==2.11.0
pylint==4.0.5
pyphen==0.17.2
pytest==9.0.2
pytest-cov==7.0.0
pytest-mock==3.15.1
python-dotenv==1.2.2
python-multipart==0.0.22
PyYAML==6.0.3
requests==2.32.5
rich==14.3.3
rich-toolkit==0.19.7
rignore==0.7.6
sentry-sdk==2.53.0
shellingham==1.5.4
SQLAlchemy==2.0.47
sqlmodel==0.0.37
starlette==0.52.1
tinycss2==1.5.1
tinyhtml5==2.0.0
tomlkit==0.14.0
typer==0.24.1
typing-inspection==0.4.2
typing_extensions==4.15.0
urllib3==2.6.3
uvicorn==0.41.0
uvloop==0.22.1
watchfiles==1.1.1
weasyprint==68.1
webencodings==0.5.1
websockets==16.0
zopfli==0.4.1

View File

@@ -1,30 +1,30 @@
from typing import Annotated
from fastapi import APIRouter, Security, HTTPException, Depends, Request, Cookie
from fastapi.responses import RedirectResponse, Response
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlmodel import Session, select
import jwt
from jwt import PyJWKClient
from src.settings import AUTH_URL, TOKEN_URL, JWKS_URL, ISSUER, LOGOUT_URL, settings
import src.users.service as service
from src.database import get_session
from src.models import UserCreate, User, UserPublic
import secrets
import requests
from typing import Annotated
from urllib.parse import urlencode
import jwt
import requests
import src.messages as messages
import src.users.service as service
from fastapi import (APIRouter, Cookie, Depends, HTTPException, Request,
Security)
from fastapi.responses import RedirectResponse, Response
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jwt import PyJWKClient
from sqlmodel import Session, select
from src.database import get_session
from src.models import User, UserCreate, UserPublic
from src.settings import (AUTH_URL, ISSUER, JWKS_URL, LOGOUT_URL, TOKEN_URL,
settings)
router = APIRouter(prefix='/auth')
jwk_client = PyJWKClient(JWKS_URL)
security = HTTPBearer()
@router.get('/logout')
def logout(
refresh_token: Annotated[str | None, Cookie()] = None,
):
def logout():
params = {
'client_id': settings.keycloak_client_id,
'post_logout_redirect_uri': settings.origins,
@@ -34,26 +34,20 @@ def logout(
key='access_token',
path='/',
secure=not settings.debug,
samesite='lax',
samesite='strict',
)
response.delete_cookie(
key='refresh_token',
path='/',
secure=not settings.debug,
samesite='lax',
samesite='strict',
)
response.delete_cookie(
key='id_token',
path='/',
secure=not settings.debug,
samesite='lax',
samesite='strict',
)
# if refresh_token:
# requests.post(LOGOUT_URL, data={
# 'client_id': settings.keycloak_client_id,
# 'client_secret': settings.keycloak_client_secret,
# 'refresh_token': refresh_token
# })
return response
@@ -67,9 +61,11 @@ def login():
'redirect_uri': settings.keycloak_redirect_uri,
'state': state,
}
request_url = requests.Request('GET', AUTH_URL, params=params).prepare().url
request_url = requests.Request(
'GET', AUTH_URL, params=params).prepare().url
return RedirectResponse(request_url)
@router.get('/callback')
def callback(code: str, session: Session = Depends(get_session)):
data = {
@@ -85,15 +81,17 @@ def callback(code: str, session: Session = Depends(get_session)):
response = requests.post(TOKEN_URL, data=data, headers=headers)
if response.status_code != 200:
raise HTTPException(
status_code=400,
detail=messages.failtogettoken
status_code=404,
detail=messages.Messages.not_found('token')
)
token_data = response.json()
id_token = token_data['id_token']
decoded_token = jwt.decode(id_token, options={'verify_signature': False})
decoded_access_token = jwt.decode(token_data['access_token'], options={'verify_signature': False})
decoded_access_token = jwt.decode(
token_data['access_token'], options={
'verify_signature': False})
resource_access = decoded_access_token.get('resource_access')
if not resource_access:
data = {
@@ -101,7 +99,7 @@ def callback(code: str, session: Session = Depends(get_session)):
'client_secret': settings.keycloak_client_secret,
'refresh_token': token_data['refresh_token'],
}
res = requests.post(LOGOUT_URL, data=data)
requests.post(LOGOUT_URL, data=data)
resp = RedirectResponse(f'{settings.origins}?userNotAllowed=true')
return resp
roles = resource_access.get(settings.keycloak_client_id)
@@ -111,13 +109,13 @@ def callback(code: str, session: Session = Depends(get_session)):
'client_secret': settings.keycloak_client_secret,
'refresh_token': token_data['refresh_token'],
}
res = requests.post(LOGOUT_URL, data=data)
requests.post(LOGOUT_URL, data=data)
resp = RedirectResponse(f'{settings.origins}?userNotAllowed=true')
return resp
user_create = UserCreate(
email=decoded_token.get('email'),
name=decoded_token.get('preferred_username'),
name=decoded_token.get('name'),
role_names=roles['roles']
)
service.get_or_create_user(session, user_create)
@@ -127,7 +125,7 @@ def callback(code: str, session: Session = Depends(get_session)):
value=token_data['access_token'],
httponly=True,
secure=not settings.debug,
samesite='lax',
samesite='strict',
max_age=settings.max_age
)
response.set_cookie(
@@ -135,7 +133,7 @@ def callback(code: str, session: Session = Depends(get_session)):
value=token_data['refresh_token'] or '',
httponly=True,
secure=not settings.debug,
samesite='lax',
samesite='strict',
max_age=30 * 24 * settings.max_age
)
response.set_cookie(
@@ -143,47 +141,69 @@ def callback(code: str, session: Session = Depends(get_session)):
value=token_data['id_token'],
httponly=True,
secure=not settings.debug,
samesite='lax',
samesite='strict',
max_age=settings.max_age
)
return response
def verify_token(token: str):
try:
signing_key = jwk_client.get_signing_key_from_jwt(token)
decoded = jwt.decode(token, options={'verify_signature': False})
payload = jwt.decode(
decoded = jwt.decode(
token,
signing_key.key,
algorithms=['RS256'],
audience=settings.keycloak_client_id,
issuer=ISSUER,
leeway=60,
)
return payload
return decoded
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail=messages.tokenexipired)
raise HTTPException(
status_code=401,
detail=messages.Messages.tokenexipired
)
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail=messages.invalidtoken)
raise HTTPException(
status_code=401,
detail=messages.Messages.invalidtoken
)
def get_current_user(request: Request, session: Session = Depends(get_session)):
def get_current_user(
request: Request,
session: Session = Depends(get_session)):
access_token = request.cookies.get('access_token')
if not access_token:
raise HTTPException(status_code=401, detail=messages.notauthenticated)
raise HTTPException(
status_code=401,
detail=messages.Messages.notauthenticated
)
payload = verify_token(access_token)
if not payload:
raise HTTPException(status_code=401, detail='aze')
raise HTTPException(
status_code=401,
detail='aze'
)
email = payload.get('email')
if not email:
raise HTTPException(status_code=401, detail=messages.notauthenticated)
raise HTTPException(
status_code=401,
detail=messages.Messages.notauthenticated
)
user = session.exec(select(User).where(User.email == email)).first()
if not user:
raise HTTPException(status_code=401, detail=messages.usernotfound)
raise HTTPException(
status_code=401,
detail=messages.Messages.not_found('user')
)
return user
@router.post('/refresh')
def refresh_token(refresh_token: Annotated[str | None, Cookie()] = None):
refresh = refresh_token
@@ -199,8 +219,8 @@ def refresh_token(refresh_token: Annotated[str | None, Cookie()] = None):
result = requests.post(TOKEN_URL, data=data, headers=headers)
if result.status_code != 200:
raise HTTPException(
status_code=400,
detail=messages.failtogettoken
status_code=404,
detail=messages.Messages.not_found('token')
)
token_data = result.json()
@@ -210,7 +230,7 @@ def refresh_token(refresh_token: Annotated[str | None, Cookie()] = None):
value=token_data['access_token'],
httponly=True,
secure=True if settings.debug == False else True,
samesite='lax',
samesite='strict',
max_age=settings.max_age
)
response.set_cookie(
@@ -218,11 +238,20 @@ def refresh_token(refresh_token: Annotated[str | None, Cookie()] = None):
value=token_data['refresh_token'] or '',
httponly=True,
secure=True if settings.debug == False else True,
samesite='lax',
max_age=4
samesite='strict',
max_age=30 * 24 * settings.max_age
)
response.set_cookie(
key='id_token',
value=token_data['id_token'],
httponly=True,
secure=not settings.debug,
samesite='strict',
max_age=settings.max_age
)
return response
@router.get('/user/me')
def me(user: UserPublic = Depends(get_current_user)):
if not user:
@@ -233,6 +262,6 @@ def me(user: UserPublic = Depends(get_current_user)):
'name': user.name,
'email': user.email,
'id': user.id,
'roles': [role.name for role in user.roles]
'roles': user.roles
}
}

View File

@@ -1,18 +1,27 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from src.database import get_session
from sqlmodel import Session
from src.contracts.generate_contract import generate_html_contract, generate_recap
from src.auth.auth import get_current_user
import src.models as models
import src.messages as messages
import src.contracts.service as service
import src.forms.service as form_service
"""Router for contract resource"""
import io
import zipfile
import src.contracts.service as service
import src.forms.service as form_service
import src.messages as messages
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from sqlmodel import Session
from src import models
from src.auth.auth import get_current_user
from src.contracts.generate_contract import (generate_html_contract,
generate_recap)
from src.database import get_session
router = APIRouter(prefix='/contracts')
def compute_recurrent_prices(products_quantities: list[dict], nb_shipment: int):
def compute_recurrent_prices(
products_quantities: list[dict],
nb_shipment: int
):
"""Compute price for recurrent products"""
result = 0
for product_quantity in products_quantities:
product = product_quantity['product']
@@ -20,25 +29,45 @@ def compute_recurrent_prices(products_quantities: list[dict], nb_shipment: int):
result += compute_product_price(product, quantity, nb_shipment)
return result
def compute_occasional_prices(occasionals: list[dict]):
"""Compute prices for occassional products"""
result = 0
for occasional in occasionals:
result += occasional['price']
return result
def compute_product_price(product: models.Product, quantity: int, nb_shipment: int = 1):
product_quantity_unit = 1 if product.unit == models.Unit.KILO else 1000
final_quantity = quantity if product.price else quantity / product_quantity_unit
final_price = product.price if product.price else product.price_kg
def compute_product_price(
product: models.Product,
quantity: int,
nb_shipment: int = 1
):
"""Compute price for a product"""
product_quantity_unit = (
1 if product.unit == models.Unit.KILO else 1000
)
final_quantity = (
quantity if product.price else quantity / product_quantity_unit
)
final_price = (
product.price if product.price else product.price_kg
)
return final_price * final_quantity * nb_shipment
def find_dict_in_list(lst, key, value):
"""Find the index of a dictionnary in a list of dictionnaries given a key
and a value.
"""
for i, dic in enumerate(lst):
if dic[key].id == value:
return i
return -1
def create_occasional_dict(contract_products: list[models.ContractProduct]):
"""Create a dictionnary of occasional products"""
result = []
for contract_product in contract_products:
existing_id = find_dict_in_list(
@@ -69,64 +98,175 @@ def create_occasional_dict(contract_products: list[models.ContractProduct]):
)
return result
@router.post('/')
@router.post('')
async def create_contract(
contract: models.ContractCreate,
session: Session = Depends(get_session),
):
"""Create contract route"""
new_contract = service.create_one(session, contract)
occasional_contract_products = list(filter(lambda contract_product: contract_product.product.type == models.ProductType.OCCASIONAL, new_contract.products))
occasional_contract_products = list(
filter(
lambda contract_product: (
contract_product.product.type == models.ProductType.OCCASIONAL
),
new_contract.products
)
)
occasionals = create_occasional_dict(occasional_contract_products)
recurrents = list(map(lambda x: {"product": x.product, "quantity": x.quantity}, filter(lambda contract_product: contract_product.product.type == models.ProductType.RECCURENT, new_contract.products)))
recurrent_price = compute_recurrent_prices(recurrents, len(new_contract.form.shipments))
recurrents = list(
map(
lambda x: {'product': x.product, 'quantity': x.quantity},
filter(
lambda contract_product: (
contract_product.product.type ==
models.ProductType.RECCURENT
),
new_contract.products
)
)
)
recurrent_price = compute_recurrent_prices(
recurrents,
len(new_contract.form.shipments)
)
price = recurrent_price + compute_occasional_prices(occasionals)
total_price = '{:10.2f}'.format(price)
cheques = list(map(lambda x: {"name": x.name, "value": x.value}, new_contract.cheques))
# TODO: send contract to referer
cheques = list(
map(
lambda x: {'name': x.name, 'value': x.value},
new_contract.cheques
)
)
try:
pdf_bytes = generate_html_contract(
new_contract,
cheques,
occasionals,
recurrents,
recurrent_price,
total_price
'{:10.2f}'.format(recurrent_price),
'{:10.2f}'.format(price)
)
pdf_file = io.BytesIO(pdf_bytes)
contract_id = f'{new_contract.firstname}_{new_contract.lastname}_{new_contract.form.productor.type}_{new_contract.form.season}'
contract_id = (
f'{new_contract.firstname}_'
f'{new_contract.lastname}_'
f'{new_contract.form.productor.type}_'
f'{new_contract.form.season}'
)
service.add_contract_file(session, new_contract.id, pdf_bytes, price)
except Exception as e:
print(e)
raise HTTPException(status_code=400, detail=messages.pdferror)
except Exception as error:
raise HTTPException(
status_code=400,
detail=messages.pdferror
) from error
return StreamingResponse(
pdf_file,
media_type='application/pdf',
headers={
'Content-Disposition': f'attachment; filename=contract_{contract_id}.pdf'
'Content-Disposition': (
f'attachment; filename=contract_{contract_id}.pdf'
)
}
)
@router.get('/', response_model=list[models.ContractPublic])
@router.get('/{form_id}/base')
async def get_base_contract_template(
form_id: int,
session: Session = Depends(get_session),
):
"""Get contract template route"""
form = form_service.get_one(session, form_id)
recurrents = [
{'product': product, 'quantity': None}
for product in form.productor.products
if product.type == models.ProductType.RECCURENT
]
occasionals = [{
'shipment': sh,
'price': None,
'products': [{'product': pr, 'quantity': None} for pr in sh.products]
} for sh in form.shipments]
empty_contract = models.ContractPublic(
firstname='',
form=form,
lastname='',
email='',
phone='',
products=[],
payment_method='cheque',
cheque_quantity=3,
total_price=0,
id=1
)
cheques = [
{'name': None, 'value': None},
{'name': None, 'value': None},
{'name': None, 'value': None}
]
try:
pdf_bytes = generate_html_contract(
empty_contract,
cheques,
occasionals,
recurrents,
)
pdf_file = io.BytesIO(pdf_bytes)
contract_id = (
f'{empty_contract.form.productor.type}_'
f'{empty_contract.form.season}'
)
except Exception as error:
raise HTTPException(
status_code=400,
detail=messages.pdferror
) from error
return StreamingResponse(
pdf_file,
media_type='application/pdf',
headers={
'Content-Disposition': (
f'attachment; filename=contract_{contract_id}.pdf'
)
}
)
@router.get('', response_model=list[models.ContractPublic])
def get_contracts(
forms: list[str] = Query([]),
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
"""Get all contracts route"""
return service.get_all(session, user, forms)
@router.get('/{id}/file')
@router.get('/{_id}/file')
def get_contract_file(
id: int,
_id: int,
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
if not service.is_allowed(session, user, id):
raise HTTPException(status_code=403, detail=messages.notallowed)
contract = service.get_one(session, id)
"""Get a contract file (in pdf) route"""
if not service.is_allowed(session, user, _id):
raise HTTPException(
status_code=403,
detail=messages.Messages.not_allowed('contract', 'get')
)
contract = service.get_one(session, _id)
if contract is None:
raise HTTPException(status_code=404, detail=messages.notfound)
filename = f'{contract.form.name.replace(' ', '_')}_{contract.form.season}_{contract.firstname}-{contract.lastname}'
raise HTTPException(
status_code=404,
detail=messages.Messages.not_found('contract')
)
filename = (
f'{contract.form.name.replace(' ', '_')}_'
f'{contract.form.season}_'
f'{contract.firstname}_'
f'{contract.lastname}'
)
return StreamingResponse(
io.BytesIO(contract.file),
media_type='application/pdf',
@@ -135,23 +275,37 @@ def get_contract_file(
}
)
@router.get('/{form_id}/files')
def get_contract_files(
form_id: int,
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
"""Get all contract files for a given form"""
if not form_service.is_allowed(session, user, form_id):
raise HTTPException(status_code=403, detail=messages.notallowed)
raise HTTPException(
status_code=403,
detail=messages.Messages.not_allowed('contracts', 'get')
)
form = form_service.get_one(session, form_id=form_id)
contracts = service.get_all(session, user, forms=[form.name])
zipped_contracts = io.BytesIO()
with zipfile.ZipFile(zipped_contracts, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
with zipfile.ZipFile(
zipped_contracts,
'a',
zipfile.ZIP_DEFLATED,
False
) as zip_file:
for contract in contracts:
contract_filename = f'{contract.form.name.replace(' ', '_')}_{contract.form.season}_{contract.firstname}-{contract.lastname}.pdf'
contract_filename = (
f'{contract.form.name.replace(' ', '_')}_'
f'{contract.form.season}_'
f'{contract.firstname}_'
f'{contract.lastname}'
)
zip_file.writestr(contract_filename, contract.file)
filename = f'{form.name.replace(" ", "_")}_{form.season}'
filename = f'{form.name.replace(' ', '_')}_{form.season}'
return StreamingResponse(
io.BytesIO(zipped_contracts.getvalue()),
media_type='application/zip',
@@ -160,39 +314,69 @@ def get_contract_files(
}
)
@router.get('/{form_id}/recap')
def get_contract_recap(
form_id: int,
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
"""Get a contract recap for a given form"""
if not form_service.is_allowed(session, user, form_id):
raise HTTPException(status_code=403, detail=messages.notallowed)
raise HTTPException(
status_code=403,
detail=messages.Messages.not_allowed('contract recap', 'get')
)
form = form_service.get_one(session, form_id=form_id)
contracts = service.get_all(session, user, forms=[form.name])
return StreamingResponse(
io.BytesIO(generate_recap(contracts, form)),
media_type='application/zip',
headers={
'Content-Disposition': f'attachment; filename=filename.ods'
'Content-Disposition': (
'attachment; filename=filename.ods'
)
}
)
@router.get('/{id}', response_model=models.ContractPublic)
def get_contract(id: int, session: Session = Depends(get_session), user: models.User = Depends(get_current_user)):
if not service.is_allowed(session, user, id):
raise HTTPException(status_code=403, detail=messages.notallowed)
result = service.get_one(session, id)
@router.get('/{_id}', response_model=models.ContractPublic)
def get_contract(
_id: int,
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
"""Get a contract route"""
if not service.is_allowed(session, user, _id):
raise HTTPException(
status_code=403,
detail=messages.Messages.not_allowed('contract', 'get')
)
result = service.get_one(session, _id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(
status_code=404,
detail=messages.Messages.not_found('contract')
)
return result
@router.delete('/{id}', response_model=models.ContractPublic)
def delete_contract(id: int, session: Session = Depends(get_session), user: models.User = Depends(get_current_user)):
if not service.is_allowed(session, user, id):
raise HTTPException(status_code=403, detail=messages.notallowed)
result = service.delete_one(session, id)
@router.delete('/{_id}', response_model=models.ContractPublic)
def delete_contract(
_id: int,
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
"""Delete contract route"""
if not service.is_allowed(session, user, _id):
raise HTTPException(
status_code=403,
detail=messages.Messages.not_allowed('contract', 'delete')
)
result = service.delete_one(session, _id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(
status_code=404,
detail=messages.Messages.not_found('contract')
)
return result

View File

@@ -1,21 +1,26 @@
import html
import io
import pathlib
import jinja2
import src.models as models
import html
from odfdo import Cell, Document, Row, Table
from src import models
from weasyprint import HTML
import io
def generate_html_contract(
contract: models.Contract,
cheques: list[dict],
occasionals: list[dict],
reccurents: list[dict],
recurrent_price: float,
total_price: float
recurrent_price: float | None = None,
total_price: float | None = None
):
template_dir = "./src/contracts/templates"
template_dir = pathlib.Path("./src/contracts/templates").resolve()
template_loader = jinja2.FileSystemLoader(searchpath=template_dir)
template_env = jinja2.Environment(loader=template_loader, autoescape=jinja2.select_autoescape(["html", "xml"]))
template_env = jinja2.Environment(
loader=template_loader, autoescape=jinja2.select_autoescape(["html", "xml"]))
template_file = "layout.html"
template = template_env.get_template(template_file)
output_text = template.render(
@@ -26,41 +31,36 @@ def generate_html_contract(
referer_email=contract.form.referer.email,
productor_name=contract.form.productor.name,
productor_address=contract.form.productor.address,
payment_methods_map={"cheque": "Ordre du chèque", "transfer": "virements"},
payment_methods_map={
"cheque": "Ordre du chèque",
"transfer": "virements"},
productor_payment_methods=contract.form.productor.payment_methods,
member_name=f'{html.escape(contract.firstname)} {html.escape(contract.lastname)}',
member_email=html.escape(contract.email),
member_phone=html.escape(contract.phone),
member_name=f'{
html.escape(
contract.firstname)} {
html.escape(
contract.lastname)}',
member_email=html.escape(
contract.email),
member_phone=html.escape(
contract.phone),
contract_start_date=contract.form.start,
contract_end_date=contract.form.end,
occasionals=occasionals,
recurrents=reccurents,
recurrent_price=recurrent_price,
total_price=total_price,
contract_payment_method={"cheque": "chèque", "transfer": "virements"}[contract.payment_method],
cheques=cheques
)
options = {
'page-size': 'Letter',
'margin-top': '0.5in',
'margin-right': '0.5in',
'margin-bottom': '0.5in',
'margin-left': '0.5in',
'encoding': "UTF-8",
'print-media-type': True,
"disable-javascript": True,
"disable-external-links": True,
'enable-local-file-access': False,
"disable-local-file-access": True,
"no-images": True,
}
contract_payment_method={
"cheque": "chèque",
"transfer": "virements"}[
contract.payment_method],
cheques=cheques)
return HTML(
string=output_text,
base_url=template_dir
base_url=template_dir,
).write_pdf()
from odfdo import Document, Table, Row, Cell
def generate_recap(
contracts: list[models.Contract],
@@ -79,4 +79,3 @@ def generate_recap(
doc.save(buffer)
return buffer.getvalue()

View File

@@ -1,28 +1,57 @@
"""Contract service responsible for read, create, update and delete contracts"""
from sqlalchemy.orm import selectinload
from sqlmodel import Session, select
import src.models as models
from src import models
def get_all(
session: Session,
user: models.User,
forms: list[str] = [],
forms: list[str] | None = None,
form_id: int | None = None,
) -> list[models.ContractPublic]:
statement = select(models.Contract)\
.join(models.Form, models.Contract.form_id == models.Form.id)\
.join(models.Productor, models.Form.productor_id == models.Productor.id)\
.where(models.Productor.type.in_([r.name for r in user.roles]))\
"""Get all contracts"""
statement = (
select(models.Contract)
.join(
models.Form,
models.Contract.form_id == models.Form.id
)
.join(
models.Productor,
models.Form.productor_id == models.Productor.id
)
.where(
models.Productor.type.in_(
[r.name for r in user.roles]
)
)
.distinct()
if len(forms) > 0:
)
if forms:
statement = statement.where(models.Form.name.in_(forms))
if form_id:
statement = statement.where(models.Form.id == form_id)
return session.exec(statement.order_by(models.Contract.id)).all()
def get_one(session: Session, contract_id: int) -> models.ContractPublic:
def get_one(
session: Session,
contract_id: int
) -> models.ContractPublic:
"""Get one contract"""
return session.get(models.Contract, contract_id)
def create_one(session: Session, contract: models.ContractCreate) -> models.ContractPublic:
contract_create = contract.model_dump(exclude_unset=True, exclude=["products", "cheques"])
def create_one(
session: Session,
contract: models.ContractCreate
) -> models.ContractPublic:
"""Create one contract"""
contract_create = contract.model_dump(
exclude_unset=True,
exclude=["products", "cheques"]
)
new_contract = models.Contract(**contract_create)
new_contract.cheques = [
@@ -45,10 +74,27 @@ def create_one(session: Session, contract: models.ContractCreate) -> models.Cont
session.add(new_contract)
session.commit()
session.refresh(new_contract)
return new_contract
def add_contract_file(session: Session, id: int, file: bytes, price: float):
statement = select(models.Contract).where(models.Contract.id == id)
statement = (
select(models.Contract)
.where(models.Contract.id == new_contract.id)
.options(
selectinload(models.Contract.form)
.selectinload(models.Form.productor)
)
)
return session.exec(statement).one()
def add_contract_file(
session: Session,
_id: int,
file: bytes,
price: float
):
"""Add a file to an existing contract"""
statement = select(models.Contract).where(models.Contract.id == _id)
result = session.exec(statement)
contract = result.first()
contract.total_price = price
@@ -58,8 +104,14 @@ def add_contract_file(session: Session, id: int, file: bytes, price: float):
session.refresh(contract)
return contract
def update_one(session: Session, id: int, contract: models.ContractUpdate) -> models.ContractPublic:
statement = select(models.Contract).where(models.Contract.id == id)
def update_one(
session: Session,
_id: int,
contract: models.ContractUpdate
) -> models.ContractPublic:
"""Update one contract"""
statement = select(models.Contract).where(models.Contract.id == _id)
result = session.exec(statement)
new_contract = result.first()
if not new_contract:
@@ -72,8 +124,13 @@ def update_one(session: Session, id: int, contract: models.ContractUpdate) -> mo
session.refresh(new_contract)
return new_contract
def delete_one(session: Session, id: int) -> models.ContractPublic:
statement = select(models.Contract).where(models.Contract.id == id)
def delete_one(
session: Session,
_id: int
) -> models.ContractPublic:
"""Delete one contract"""
statement = select(models.Contract).where(models.Contract.id == _id)
result = session.exec(statement)
contract = result.first()
if not contract:
@@ -83,11 +140,29 @@ def delete_one(session: Session, id: int) -> models.ContractPublic:
session.commit()
return result
def is_allowed(session: Session, user: models.User, id: int) -> bool:
statement = select(models.Contract)\
.join(models.Form, models.Contract.form_id == models.Form.id)\
.join(models.Productor, models.Form.productor_id == models.Productor.id)\
.where(models.Contract.id == id)\
.where(models.Productor.type.in_([r.name for r in user.roles]))\
def is_allowed(
session: Session,
user: models.User,
_id: int
) -> bool:
"""Determine if a user is allowed to access a contract by id"""
statement = (
select(models.Contract)
.join(
models.Form,
models.Contract.form_id == models.Form.id
)
.join(
models.Productor,
models.Form.productor_id == models.Productor.id
)
.where(models.Contract.id == _id)
.where(
models.Productor.type.in_(
[r.name for r in user.roles]
)
)
.distinct()
)
return len(session.exec(statement).all()) > 0

View File

@@ -151,10 +151,6 @@
<th>Saison du contrat</th>
<td>{{contract_season}}</td>
</tr>
<tr>
<th>Type de contrat</th>
<td>{{contract_type}}</td>
</tr>
<tr>
<th>Référent·e</th>
<td>{{referer_name}}</td>
@@ -278,14 +274,14 @@
else ""}}
</td>
<td>
{{rec.quantity}}{{"g" if rec.product.unit == "1" else "kg" if
{{rec.quantity if rec.quantity != None else ""}}{{"g" if rec.product.unit == "1" else "kg" if
rec.product.unit == "2" else "p" }}
</td>
</tr>
{% endfor %}
<tr>
<th scope="row" colspan="4">Total</th>
<td>{{recurrent_price}}€</td>
<td>{{recurrent_price if recurrent_price else ""}}€</td>
</tr>
</tbody>
</table>
@@ -321,14 +317,15 @@
product.product.quantity_unit != None else ""}}
</td>
<td>
{{product.quantity}}{{"g" if product.product.unit == "1" else
{{product.quantity if product.quantity != None
else ""}}{{"g" if product.product.unit == "1" else
"kg" if product.product.unit == "2" else "p" }}
</td>
</tr>
{% endfor%}
<tr>
<th scope="row" colspan="4">Total</th>
<td>{{occasional.price}}€</td>
<td>{{occasional.price if occasional.price else ""}}€</td>
</tr>
</tbody>
</table>
@@ -337,7 +334,7 @@
{% endif %}
<div class="total-box">
<div class="total-label">Prix Total :</div>
<div class="total-price">{{total_price}}€</div>
<div class="total-price">{{total_price if total_price else ""}}€</div>
</div>
<h4>Paiement par {{contract_payment_method}}</h4>
{% if contract_payment_method == "chèque" %}
@@ -346,14 +343,14 @@
<thead>
<tr>
{% for cheque in cheques %}
<th>Cheque n°{{cheque.name}}</th>
<th>Cheque n°{{cheque.name if cheque.name else ""}}</th>
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
{% for cheque in cheques %}
<td>{{cheque.value}}€</td>
<td>{{cheque.value if cheque.value else ""}}€</td>
{% endfor %}
</tr>
</tbody>

View File

@@ -1,11 +1,14 @@
from sqlmodel import create_engine, SQLModel, Session
from sqlmodel import Session, SQLModel, create_engine
from src.settings import settings
engine = create_engine(f'postgresql://{settings.db_user}:{settings.db_pass}@{settings.db_host}:54321/{settings.db_name}')
engine = create_engine(
f'postgresql://{settings.db_user}:{settings.db_pass}@{settings.db_host}:5432/{settings.db_name}')
def get_session():
with Session(engine) as session:
yield session
def create_all_tables():
SQLModel.metadata.create_all(engine)

View File

@@ -0,0 +1,26 @@
"""Forms module exceptions"""
import logging
class FormServiceError(Exception):
"""Form service exception"""
def __init__(self, message: str):
super().__init__(message)
logging.error('FormService : %s', message)
class UserNotFoundError(FormServiceError):
pass
class ProductorNotFoundError(FormServiceError):
pass
class FormNotFoundError(FormServiceError):
pass
class FormCreateError(FormServiceError):
def __init__(self, message: str, field: str | None = None):
super().__init__(message)
self.field = field

View File

@@ -1,55 +1,108 @@
from fastapi import APIRouter, HTTPException, Depends, Query
import src.messages as messages
import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.forms.exceptions as exceptions
import src.forms.service as service
import src.messages as messages
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session
from src import models
from src.auth.auth import get_current_user
from src.database import get_session
router = APIRouter(prefix='/forms')
@router.get('/', response_model=list[models.FormPublic])
@router.get('', response_model=list[models.FormPublic])
async def get_forms(
seasons: list[str] = Query([]),
productors: list[str] = Query([]),
current_season: bool = False,
session: Session = Depends(get_session)
session: Session = Depends(get_session),
):
return service.get_all(session, seasons, productors, current_season)
@router.get('/{id}', response_model=models.FormPublic)
async def get_form(id: int, session: Session = Depends(get_session)):
result = service.get_one(session, id)
@router.get('/referents', response_model=list[models.FormPublic])
async def get_forms_filtered(
seasons: list[str] = Query([]),
productors: list[str] = Query([]),
current_season: bool = False,
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
return service.get_all(session, seasons, productors, current_season, user)
@router.get('/{_id}', response_model=models.FormPublic)
async def get_form(
_id: int,
session: Session = Depends(get_session)
):
result = service.get_one(session, _id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(
status_code=404,
detail=messages.Messages.not_found('form')
)
return result
@router.post('/', response_model=models.FormPublic)
@router.post('', response_model=models.FormPublic)
async def create_form(
form: models.FormCreate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.create_one(session, form)
if not service.is_allowed(session, user, form=form):
raise HTTPException(
status_code=403,
detail=messages.Messages.not_allowed('forms', 'update')
)
try:
form = service.create_one(session, form)
except exceptions.ProductorNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
except exceptions.UserNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
except exceptions.FormCreateError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
return form
@router.put('/{id}', response_model=models.FormPublic)
@router.put('/{_id}', response_model=models.FormPublic)
async def update_form(
id: int, form: models.FormUpdate,
_id: int,
form: models.FormUpdate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.update_one(session, id, form)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
if not service.is_allowed(session, user, _id=_id):
raise HTTPException(
status_code=403,
detail=messages.Messages.not_allowed('forms', 'update')
)
try:
result = service.update_one(session, _id, form)
except exceptions.FormNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
except exceptions.ProductorNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
except exceptions.UserNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
return result
@router.delete('/{id}', response_model=models.FormPublic)
@router.delete('/{_id}', response_model=models.FormPublic)
async def delete_form(
id: int,
_id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
if not service.is_allowed(session, user, _id=_id):
raise HTTPException(
status_code=403,
detail=messages.Messages.not_allowed('forms', 'delete')
)
try:
result = service.delete_one(session, _id)
except exceptions.FormNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
return result

View File

@@ -1,26 +1,41 @@
from sqlmodel import Session, select
import src.models as models
import src.forms.exceptions as exceptions
import src.messages as messages
from sqlalchemy import func
from sqlmodel import Session, select
from src import models
def get_all(
session: Session,
seasons: list[str],
productors: list[str],
current_season: bool,
user: models.User = None
) -> list[models.FormPublic]:
statement = select(models.Form)
if user:
statement = statement .join(
models.Productor,
models.Form.productor_id == models.Productor.id) .where(
models.Productor.type.in_(
[
r.name for r in user.roles])) .distinct()
if len(seasons) > 0:
statement = statement.where(models.Form.season.in_(seasons))
if len(productors) > 0:
statement = statement.join(models.Productor).where(models.Productor.name.in_(productors))
statement = statement.join(
models.Productor).where(
models.Productor.name.in_(productors))
if not user:
statement = statement.where(models.Form.visible)
if current_season:
subquery = (
select(
models.Productor.type,
func.max(models.Form.start).label("max_start")
)
.join(models.Form)\
.group_by(models.Productor.type)\
.join(models.Form)
.group_by(models.Productor.type)
.subquery()
)
statement = select(models.Form)\
@@ -29,13 +44,26 @@ def get_all(
(models.Productor.type == subquery.c.type) &
(models.Form.start == subquery.c.max_start)
)
if not user:
statement = statement.where(models.Form.visible)
return session.exec(statement.order_by(models.Form.name)).all()
return session.exec(statement.order_by(models.Form.name)).all()
def get_one(session: Session, form_id: int) -> models.FormPublic:
return session.get(models.Form, form_id)
def create_one(session: Session, form: models.FormCreate) -> models.FormPublic:
if not form:
raise exceptions.FormCreateError(
messages.Messages.invalid_input(
'form', 'input cannot be None'))
if not session.get(models.Productor, form.productor_id):
raise exceptions.ProductorNotFoundError(
messages.Messages.not_found('productor'))
if not session.get(models.User, form.referer_id):
raise exceptions.UserNotFoundError(messages.Messages.not_found('user'))
form_create = form.model_dump(exclude_unset=True)
new_form = models.Form(**form_create)
session.add(new_form)
@@ -43,12 +71,22 @@ def create_one(session: Session, form: models.FormCreate) -> models.FormPublic:
session.refresh(new_form)
return new_form
def update_one(session: Session, id: int, form: models.FormUpdate) -> models.FormPublic:
statement = select(models.Form).where(models.Form.id == id)
def update_one(
session: Session,
_id: int,
form: models.FormUpdate) -> models.FormPublic:
statement = select(models.Form).where(models.Form.id == _id)
result = session.exec(statement)
new_form = result.first()
if not new_form:
return None
raise exceptions.FormNotFoundError(messages.Messages.not_found('form'))
if form.productor_id and not session.get(
models.Productor, form.productor_id):
raise exceptions.ProductorNotFoundError(
messages.Messages.not_found('productor'))
if form.referer_id and not session.get(models.User, form.referer_id):
raise exceptions.UserNotFoundError(messages.Messages.not_found('user'))
form_updates = form.model_dump(exclude_unset=True)
for key, value in form_updates.items():
setattr(new_form, key, value)
@@ -57,21 +95,44 @@ def update_one(session: Session, id: int, form: models.FormUpdate) -> models.For
session.refresh(new_form)
return new_form
def delete_one(session: Session, id: int) -> models.FormPublic:
statement = select(models.Form).where(models.Form.id == id)
def delete_one(session: Session, _id: int) -> models.FormPublic:
statement = select(models.Form).where(models.Form.id == _id)
result = session.exec(statement)
form = result.first()
if not form:
return None
raise exceptions.FormNotFoundError(messages.Messages.not_found('form'))
result = models.FormPublic.model_validate(form)
session.delete(form)
session.commit()
return result
def is_allowed(session: Session, user: models.User, id: int) -> bool:
statement = select(models.Form)\
.join(models.Productor, models.Form.productor_id == models.Productor.id)\
.where(models.Form.id == id)\
.where(models.Productor.type.in_([r.name for r in user.roles]))\
def is_allowed(
session: Session,
user: models.User,
_id: int = None,
form: models.FormCreate = None
) -> bool:
if not _id:
statement = (
select(models.Productor)
.where(models.Productor.id == form.productor_id)
)
productor = session.exec(statement).first()
return productor.type in [r.name for r in user.roles]
statement = (
select(models.Form)
.join(
models.Productor,
models.Form.productor_id == models.Productor.id
)
.where(models.Form.id == _id)
.where(
models.Productor.type.in_(
[r.name for r in user.roles]
)
)
.distinct()
)
return len(session.exec(statement).all()) > 0

View File

@@ -1,21 +1,18 @@
from sqlmodel import SQLModel
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from src.templates.templates import router as template_router
from src.auth.auth import router as auth_router
from src.contracts.contracts import router as contracts_router
from src.forms.forms import router as forms_router
from src.productors.productors import router as productors_router
from src.products.products import router as products_router
from src.users.users import router as users_router
from src.auth.auth import router as auth_router
from src.shipments.shipments import router as shipment_router
from src.settings import settings
from src.database import engine
from src.shipments.shipments import router as shipment_router
from src.templates.templates import router as template_router
from src.users.users import router as users_router
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[
@@ -27,14 +24,11 @@ app.add_middleware(
expose_headers=['x-nbpage', 'Content-Disposition']
)
app.include_router(template_router)
app.include_router(contracts_router)
app.include_router(forms_router)
app.include_router(productors_router)
app.include_router(products_router)
app.include_router(users_router)
app.include_router(auth_router)
app.include_router(shipment_router)
SQLModel.metadata.create_all(engine)
app.include_router(template_router, prefix="/api")
app.include_router(contracts_router, prefix="/api")
app.include_router(forms_router, prefix="/api")
app.include_router(productors_router, prefix="/api")
app.include_router(products_router, prefix="/api")
app.include_router(users_router, prefix="/api")
app.include_router(auth_router, prefix="/api")
app.include_router(shipment_router, prefix="/api")

View File

@@ -1,10 +1,20 @@
notfound = "Resource was not found."
pdferror = "An error occured during PDF generation please contact administrator"
tokenexipired = "Token expired"
invalidtoken = "Invalid token"
notauthenticated = "Not authenticated"
usernotfound = "User not found"
userloggedout = "User logged out"
failtogettoken = "Failed to get token"
unauthorized = "Unauthorized"
notallowed = "Not Allowed"
pdferror = 'An error occured during PDF generation please contact administrator'
class Messages:
unauthorized = 'User is Unauthorized'
notauthenticated = 'User is not authenticated'
tokenexipired = 'Token has expired'
invalidtoken = 'Token is invalid'
@staticmethod
def not_found(resource: str) -> str:
return f'{resource.capitalize()} not found'
@staticmethod
def invalid_input(resource: str, reason: str = "") -> str:
return f'Invalid {resource} input {':' if reason else ""} {reason}'
@staticmethod
def not_allowed(resource: str, action: str) -> str:
return f'User is not allowed to {action} this {resource}'

View File

@@ -1,98 +1,136 @@
from sqlmodel import Field, SQLModel, Relationship, Column, LargeBinary
import datetime
from enum import StrEnum
from typing import Optional
import datetime
from sqlmodel import Column, Field, LargeBinary, Relationship, SQLModel
class ContractType(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
id: int | None = Field(
default=None,
primary_key=True
)
name: str
class UserContractTypeLink(SQLModel, table=True):
user_id: int = Field(foreign_key="user.id", primary_key=True)
contract_type_id: int = Field(foreign_key="contracttype.id", primary_key=True)
user_id: int = Field(
foreign_key='user.id',
primary_key=True
)
contract_type_id: int = Field(
foreign_key='contracttype.id',
primary_key=True
)
class UserBase(SQLModel):
name: str
email: str
class UserPublic(UserBase):
id: int
roles: list[ContractType]
class User(UserBase, table=True):
id: int | None = Field(default=None, primary_key=True)
roles: list[ContractType] = Relationship(
link_model=UserContractTypeLink
)
class UserUpdate(SQLModel):
name: str | None
email: str | None
role_names: list[str] | None
class UserCreate(UserBase):
role_names: list[str] | None
class PaymentMethodBase(SQLModel):
name: str
details: str
max: int | None
class PaymentMethod(PaymentMethodBase, table=True):
id: int | None = Field(default=None, primary_key=True)
productor_id: int = Field(foreign_key="productor.id", ondelete="CASCADE")
productor: Optional["Productor"] = Relationship(
back_populates="payment_methods",
productor_id: int = Field(foreign_key='productor.id', ondelete='CASCADE')
productor: Optional['Productor'] = Relationship(
back_populates='payment_methods',
)
class PaymentMethodPublic(PaymentMethodBase):
id: int
productor: Optional["Productor"]
productor: Optional['Productor']
class ProductorBase(SQLModel):
name: str
address: str
type: str
class ProductorPublic(ProductorBase):
id: int
products: list["Product"] = []
payment_methods: list["PaymentMethod"] = []
products: list['Product'] = Field(default_factory=list)
payment_methods: list['PaymentMethod'] = Field(default_factory=list)
class Productor(ProductorBase, table=True):
id: int | None = Field(default=None, primary_key=True)
products: list["Product"] = Relationship(
products: list['Product'] = Relationship(
back_populates='productor',
sa_relationship_kwargs={
"order_by": "Product.name"
'order_by': 'Product.name'
},
)
payment_methods: list["PaymentMethod"] = Relationship(
back_populates="productor",
payment_methods: list['PaymentMethod'] = Relationship(
back_populates='productor',
cascade_delete=True
)
class ProductorUpdate(SQLModel):
name: str | None
address: str | None
payment_methods: list["PaymentMethod"] = []
payment_methods: list['PaymentMethod'] = Field(default_factory=list)
type: str | None
class ProductorCreate(ProductorBase):
payment_methods: list["PaymentMethod"] = []
payment_methods: list['PaymentMethod'] = Field(default_factory=list)
class Unit(StrEnum):
GRAMS = "1"
KILO = "2"
PIECE = "3"
GRAMS = '1'
KILO = '2'
PIECE = '3'
class ProductType(StrEnum):
OCCASIONAL = "1"
RECCURENT = "2"
OCCASIONAL = '1'
RECCURENT = '2'
class ShipmentProductLink(SQLModel, table=True):
shipment_id: Optional[int] = Field(default=None, foreign_key="shipment.id", primary_key=True)
product_id: Optional[int] = Field(default=None, foreign_key="product.id", primary_key=True)
shipment_id: Optional[int] = Field(
default=None,
foreign_key='shipment.id',
primary_key=True
)
product_id: Optional[int] = Field(
default=None,
foreign_key='product.id',
primary_key=True
)
class ProductBase(SQLModel):
name: str
@@ -102,17 +140,31 @@ class ProductBase(SQLModel):
quantity: float | None
quantity_unit: str | None
type: ProductType
productor_id: int | None = Field(default=None, foreign_key="productor.id")
productor_id: int | None = Field(
default=None,
foreign_key='productor.id'
)
class ProductPublic(ProductBase):
id: int
productor: Productor | None
shipments: list["Shipment"] | None
shipments: list['Shipment'] | None
class Product(ProductBase, table=True):
id: int | None = Field(default=None, primary_key=True)
shipments: list["Shipment"] = Relationship(back_populates="products", link_model=ShipmentProductLink)
productor: Optional[Productor] = Relationship(back_populates="products")
id: int | None = Field(
default=None,
primary_key=True
)
shipments: list['Shipment'] = Relationship(
back_populates='products',
link_model=ShipmentProductLink
)
productor: Optional[Productor] = Relationship(
back_populates='products'
)
class ProductUpdate(SQLModel):
name: str | None
@@ -124,40 +176,46 @@ class ProductUpdate(SQLModel):
productor_id: int | None
type: ProductType | None
class ProductCreate(ProductBase):
pass
class FormBase(SQLModel):
name: str
productor_id: int | None = Field(default=None, foreign_key="productor.id")
referer_id: int | None = Field(default=None, foreign_key="user.id")
productor_id: int | None = Field(default=None, foreign_key='productor.id')
referer_id: int | None = Field(default=None, foreign_key='user.id')
season: str
start: datetime.date
end: datetime.date
minimum_shipment_value: float | None
visible: bool
class FormPublic(FormBase):
id: int
productor: ProductorPublic | None
referer: User | None
shipments: list["ShipmentPublic"] = []
shipments: list['ShipmentPublic'] = Field(default_factory=list)
class Form(FormBase, table=True):
id: int | None = Field(default=None, primary_key=True)
productor: Optional['Productor'] = Relationship()
referer: Optional['User'] = Relationship()
shipments: list["Shipment"] = Relationship(
back_populates="form",
shipments: list['Shipment'] = Relationship(
back_populates='form',
cascade_delete=True,
sa_relationship_kwargs={
"order_by": "Shipment.name"
'order_by': 'Shipment.name'
},
)
contracts: list["Contract"] = Relationship(
back_populates="form",
contracts: list['Contract'] = Relationship(
back_populates='form',
cascade_delete=True
)
class FormUpdate(SQLModel):
name: str | None
productor_id: int | None
@@ -166,36 +224,46 @@ class FormUpdate(SQLModel):
start: datetime.date | None
end: datetime.date | None
minimum_shipment_value: float | None
visible: bool | None
class FormCreate(FormBase):
pass
class TemplateBase(SQLModel):
pass
class TemplatePublic(TemplateBase):
id: int
class Template(TemplateBase, table=True):
id: int | None = Field(default=None, primary_key=True)
class TemplateUpdate(SQLModel):
pass
class TemplateCreate(TemplateBase):
pass
class ChequeBase(SQLModel):
name: str
value: str
class Cheque(ChequeBase, table=True):
id: int | None = Field(default=None, primary_key=True)
contract_id: int = Field(foreign_key="contract.id", ondelete="CASCADE")
contract: Optional["Contract"] = Relationship(
back_populates="cheques",
contract_id: int = Field(foreign_key='contract.id', ondelete='CASCADE')
contract: Optional['Contract'] = Relationship(
back_populates='cheques',
)
class ContractBase(SQLModel):
firstname: str
lastname: str
@@ -204,105 +272,122 @@ class ContractBase(SQLModel):
payment_method: str
cheque_quantity: int
class Contract(ContractBase, table=True):
id: int | None = Field(default=None, primary_key=True)
form_id: int = Field(
foreign_key="form.id",
foreign_key='form.id',
nullable=False,
ondelete="CASCADE"
ondelete='CASCADE'
)
products: list["ContractProduct"] = Relationship(
back_populates="contract",
products: list['ContractProduct'] = Relationship(
back_populates='contract',
cascade_delete=True
)
form: Optional[Form] = Relationship(back_populates="contracts")
form: Form = Relationship(back_populates='contracts')
cheques: list[Cheque] = Relationship(
back_populates="contract",
back_populates='contract',
cascade_delete=True
)
file: bytes = Field(sa_column=Column(LargeBinary))
total_price: float | None
class ContractCreate(ContractBase):
products: list["ContractProductCreate"] = []
cheques: list["Cheque"] = []
products: list['ContractProductCreate'] = Field(default_factory=list)
cheques: list['Cheque'] = Field(default_factory=list)
form_id: int
class ContractUpdate(SQLModel):
file: bytes
class ContractPublic(ContractBase):
id: int
products: list["ContractProduct"] = []
products: list['ContractProduct'] = Field(default_factory=list)
form: Form
total_price: float | None
# file: bytes
class ContractProductBase(SQLModel):
product_id: int = Field(
foreign_key="product.id",
foreign_key='product.id',
nullable=False,
ondelete="CASCADE"
ondelete='CASCADE'
)
shipment_id: int | None = Field(
default=None,
foreign_key="shipment.id",
foreign_key='shipment.id',
nullable=True,
ondelete="CASCADE"
ondelete='CASCADE'
)
quantity: float
class ContractProduct(ContractProductBase, table=True):
id: int | None = Field(default=None, primary_key=True)
contract_id: int = Field(
foreign_key="contract.id",
foreign_key='contract.id',
nullable=False,
ondelete="CASCADE"
ondelete='CASCADE'
)
contract: Optional["Contract"] = Relationship(back_populates="products")
product: Optional["Product"] = Relationship()
shipment: Optional["Shipment"] = Relationship()
contract: Optional['Contract'] = Relationship(back_populates='products')
product: Optional['Product'] = Relationship()
shipment: Optional['Shipment'] = Relationship()
class ContractProductPublic(ContractProductBase):
id: int
quantity: float
contract: Contract
product: Product
shipment: Optional["Shipment"]
shipment: Optional['Shipment']
class ContractProductCreate(ContractProductBase):
pass
class ContractProductUpdate(ContractProductBase):
pass
class ShipmentBase(SQLModel):
name: str
date: datetime.date
form_id: int | None = Field(default=None, foreign_key="form.id", ondelete="CASCADE")
form_id: int | None = Field(
default=None,
foreign_key='form.id',
ondelete='CASCADE')
class ShipmentPublic(ShipmentBase):
id: int
products: list[Product] = []
products: list[Product] = Field(default_factory=list)
form: Form | None
class Shipment(ShipmentBase, table=True):
id: int | None = Field(default=None, primary_key=True)
products: list[Product] = Relationship(
back_populates="shipments",
back_populates='shipments',
link_model=ShipmentProductLink,
sa_relationship_kwargs={
"order_by": "Product.name"
'order_by': 'Product.name'
},
)
form: Optional[Form] = Relationship(back_populates="shipments")
form: Optional[Form] = Relationship(back_populates='shipments')
class ShipmentUpdate(SQLModel):
name: str | None
date: str | None
product_ids: list[int] | None = []
date: datetime.date | None
product_ids: list[int] | None = Field(default_factory=list)
class ShipmentCreate(ShipmentBase):
product_ids: list[int] = []
product_ids: list[int] = Field(default_factory=list)
form_id: int

View File

@@ -0,0 +1,17 @@
import logging
class ProductorServiceError(Exception):
def __init__(self, message: str):
super().__init__(message)
logging.error('ProductorService : %s', message)
class ProductorNotFoundError(ProductorServiceError):
pass
class ProductorCreateError(ProductorServiceError):
def __init__(self, message: str, field: str | None = None):
super().__init__(message)
self.field = field

View File

@@ -1,59 +1,74 @@
from fastapi import APIRouter, HTTPException, Depends, Query
import src.messages as messages
import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.productors.exceptions as exceptions
import src.productors.service as service
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session
from src import models
from src.auth.auth import get_current_user
from src.database import get_session
router = APIRouter(prefix='/productors')
@router.get('/', response_model=list[models.ProductorPublic])
@router.get('', response_model=list[models.ProductorPublic])
def get_productors(
names: list[str] = Query([]),
types: list[str] = Query([]),
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.get_all(session, names, types)
return service.get_all(session, user, names, types)
@router.get('/{id}', response_model=models.ProductorPublic)
@router.get('/{_id}', response_model=models.ProductorPublic)
def get_productor(
id: int,
_id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.get_one(session, id)
result = service.get_one(session, _id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(
status_code=404,
detail=messages.Messages.not_found('productor')
)
return result
@router.post('/', response_model=models.ProductorPublic)
@router.post('', response_model=models.ProductorPublic)
def create_productor(
productor: models.ProductorCreate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.create_one(session, productor)
try:
result = service.create_one(session, productor)
except exceptions.ProductorCreateError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
return result
@router.put('/{id}', response_model=models.ProductorPublic)
@router.put('/{_id}', response_model=models.ProductorPublic)
def update_productor(
id: int, productor: models.ProductorUpdate,
_id: int, productor: models.ProductorUpdate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.update_one(session, id, productor)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
try:
result = service.update_one(session, _id, productor)
except exceptions.ProductorNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
return result
@router.delete('/{id}', response_model=models.ProductorPublic)
@router.delete('/{_id}', response_model=models.ProductorPublic)
def delete_productor(
id: int,
_id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
try:
result = service.delete_one(session, _id)
except exceptions.ProductorNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
return result

View File

@@ -1,23 +1,38 @@
import src.messages as messages
import src.productors.exceptions as exceptions
from sqlmodel import Session, select
import src.models as models
from src import models
def get_all(
session: Session,
user: models.User,
names: list[str],
types: list[str]
) -> list[models.ProductorPublic]:
statement = select(models.Productor)
statement = select(models.Productor)\
.where(models.Productor.type.in_([r.name for r in user.roles]))\
.distinct()
if len(names) > 0:
statement = statement.where(models.Productor.name.in_(names))
if len(types) > 0:
statement = statement.where(models.Productor.type.in_(types))
return session.exec(statement.order_by(models.Productor.name)).all()
def get_one(session: Session, productor_id: int) -> models.ProductorPublic:
return session.get(models.Productor, productor_id)
def create_one(session: Session, productor: models.ProductorCreate) -> models.ProductorPublic:
productor_create = productor.model_dump(exclude_unset=True, exclude="payment_methods")
def create_one(
session: Session,
productor: models.ProductorCreate) -> models.ProductorPublic:
if not productor:
raise exceptions.ProductorCreateError(
messages.Messages.invalid_input(
'productor', 'input cannot be None'))
productor_create = productor.model_dump(
exclude_unset=True, exclude='payment_methods')
new_productor = models.Productor(**productor_create)
new_productor.payment_methods = [
@@ -32,25 +47,31 @@ def create_one(session: Session, productor: models.ProductorCreate) -> models.Pr
session.refresh(new_productor)
return new_productor
def update_one(session: Session, id: int, productor: models.ProductorUpdate) -> models.ProductorPublic:
def update_one(
session: Session,
id: int,
productor: models.ProductorUpdate) -> models.ProductorPublic:
statement = select(models.Productor).where(models.Productor.id == id)
result = session.exec(statement)
new_productor = result.first()
if not new_productor:
return None
raise exceptions.ProductorNotFoundError(
messages.Messages.not_found('productor'))
productor_updates = productor.model_dump(exclude_unset=True)
if "payment_methods" in productor_updates:
if 'payment_methods' in productor_updates:
new_productor.payment_methods.clear()
for pm in productor_updates["payment_methods"]:
for pm in productor_updates['payment_methods']:
new_productor.payment_methods.append(
models.PaymentMethod(
name=pm["name"],
details=pm["details"],
productor_id=id
name=pm['name'],
details=pm['details'],
productor_id=id,
max=pm['max']
)
)
del productor_updates["payment_methods"]
del productor_updates['payment_methods']
for key, value in productor_updates.items():
setattr(new_productor, key, value)
@@ -59,13 +80,31 @@ def update_one(session: Session, id: int, productor: models.ProductorUpdate) ->
session.refresh(new_productor)
return new_productor
def delete_one(session: Session, id: int) -> models.ProductorPublic:
statement = select(models.Productor).where(models.Productor.id == id)
result = session.exec(statement)
productor = result.first()
if not productor:
return None
raise exceptions.ProductorNotFoundError(
messages.Messages.not_found('productor'))
result = models.ProductorPublic.model_validate(productor)
session.delete(productor)
session.commit()
return result
def is_allowed(
session: Session,
user: models.User,
_id: int,
productor: models.ProductorCreate
) -> bool:
if not _id:
return productor.type in [r.name for r in user.roles]
statement = (
select(models.Productor)
.where(models.Productor.id == _id)
.where(models.Productor.type.in_([r.name for r in user.roles]))
.distinct()
)
return len(session.exec(statement).all()) > 0

View File

@@ -0,0 +1,17 @@
class ProductServiceError(Exception):
def __init__(self, message: str):
super().__init__(message)
class ProductorNotFoundError(ProductServiceError):
pass
class ProductNotFoundError(ProductServiceError):
pass
class ProductCreateError(ProductServiceError):
def __init__(self, message: str, field: str | None = None):
super().__init__(message)
self.field = field

View File

@@ -1,13 +1,16 @@
from fastapi import APIRouter, HTTPException, Depends, Query
import src.messages as messages
import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.products.exceptions as exceptions
import src.products.service as service
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session
from src import models
from src.auth.auth import get_current_user
from src.database import get_session
router = APIRouter(prefix='/products')
#user=Depends(get_current_user)
@router.get('/', response_model=list[models.ProductPublic], )
@router.get('', response_model=list[models.ProductPublic], )
def get_products(
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session),
@@ -17,11 +20,13 @@ def get_products(
):
return service.get_all(
session,
user,
names,
productors,
types,
)
@router.get('/{id}', response_model=models.ProductPublic)
def get_product(
id: int,
@@ -30,16 +35,25 @@ def get_product(
):
result = service.get_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(status_code=404,
detail=messages.Messages.not_found('product'))
return result
@router.post('/', response_model=models.ProductPublic)
@router.post('', response_model=models.ProductPublic)
def create_product(
product: models.ProductCreate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.create_one(session, product)
try:
result = service.create_one(session, product)
except exceptions.ProductCreateError as error:
raise HTTPException(status_code=400, detail=str(error))
except exceptions.ProductorNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error))
return result
@router.put('/{id}', response_model=models.ProductPublic)
def update_product(
@@ -47,18 +61,23 @@ def update_product(
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
try:
result = service.update_one(session, id, product)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
except exceptions.ProductNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error))
except exceptions.ProductorNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error))
return result
@router.delete('/{id}', response_model=models.ProductPublic)
def delete_product(
id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
try:
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
except exceptions.ProductNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error))
return result

View File

@@ -1,25 +1,46 @@
import src.messages as messages
import src.products.exceptions as exceptions
from sqlmodel import Session, select
import src.models as models
from src import models
def get_all(
session: Session,
user: models.User,
names: list[str],
productors: list[str],
types: list[str],
) -> list[models.ProductPublic]:
statement = select(models.Product)
statement = select(
models.Product) .join(
models.Productor,
models.Product.productor_id == models.Productor.id) .where(
models.Productor.type.in_(
[
r.name for r in user.roles])) .distinct()
if len(names) > 0:
statement = statement.where(models.Product.name.in_(names))
if len(productors) > 0:
statement = statement.join(models.Productor).where(models.Productor.name.in_(productors))
statement = statement.where(models.Productor.name.in_(productors))
if len(types) > 0:
statement = statement.where(models.Product.type.in_(types))
return session.exec(statement.order_by(models.Product.name)).all()
def get_one(session: Session, product_id: int) -> models.ProductPublic:
return session.get(models.Product, product_id)
def create_one(session: Session, product: models.ProductCreate) -> models.ProductPublic:
def create_one(
session: Session,
product: models.ProductCreate) -> models.ProductPublic:
if not product:
raise exceptions.ProductCreateError(
messages.Messages.invalid_input(
'product', 'input cannot be None'))
if not session.get(models.Productor, product.productor_id):
raise exceptions.ProductorNotFoundError(
messages.Messages.not_found('productor'))
product_create = product.model_dump(exclude_unset=True)
new_product = models.Product(**product_create)
session.add(new_product)
@@ -27,12 +48,22 @@ def create_one(session: Session, product: models.ProductCreate) -> models.Produc
session.refresh(new_product)
return new_product
def update_one(session: Session, id: int, product: models.ProductUpdate) -> models.ProductPublic:
def update_one(
session: Session,
id: int,
product: models.ProductUpdate) -> models.ProductPublic:
statement = select(models.Product).where(models.Product.id == id)
result = session.exec(statement)
new_product = result.first()
if not new_product:
return None
raise exceptions.ProductNotFoundError(
messages.Messages.not_found('product'))
if product.productor_id and not session.get(
models.Productor, product.productor_id):
raise exceptions.ProductorNotFoundError(
messages.Messages.not_found('productor'))
product_updates = product.model_dump(exclude_unset=True)
for key, value in product_updates.items():
setattr(new_product, key, value)
@@ -42,13 +73,44 @@ def update_one(session: Session, id: int, product: models.ProductUpdate) -> mode
session.refresh(new_product)
return new_product
def delete_one(session: Session, id: int) -> models.ProductPublic:
statement = select(models.Product).where(models.Product.id == id)
result = session.exec(statement)
product = result.first()
if not product:
return None
raise exceptions.ProductNotFoundError(
messages.Messages.not_found('product'))
result = models.ProductPublic.model_validate(product)
session.delete(product)
session.commit()
return result
def is_allowed(
session: Session,
user: models.User,
_id: int,
product: models.ProductCreate
) -> bool:
if not _id:
statement = (
select(models.Product)
.join(
models.Productor,
models.Product.productor_id == models.Productor.id
)
.where(models.Product.id == product.productor_id)
)
productor = session.exec(statement).first()
return productor.type in [r.name for r in user.roles]
statement = (
select(models.Product)
.join(
models.Productor,
models.Product.productor_id == models.Productor.id
)
.where(models.Product.id == _id)
.where(models.Productor.type.in_([r.name for r in user.roles]))
.distinct()
)
return len(session.exec(statement).all()) > 0

View File

@@ -1,4 +1,5 @@
from pydantic_settings import BaseSettings
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
origins: str
@@ -16,13 +17,25 @@ class Settings(BaseSettings):
max_age: int
debug: bool
class Config:
env_file = "../.env"
model_config = SettingsConfigDict(
env_file='../.env'
)
settings = Settings()
AUTH_URL = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}/protocol/openid-connect/auth"
TOKEN_URL = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}/protocol/openid-connect/token"
ISSUER = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}"
JWKS_URL = f"{ISSUER}/protocol/openid-connect/certs"
LOGOUT_URL = f'{settings.keycloak_server}/realms/{settings.keycloak_realm}/protocol/openid-connect/logout'
AUTH_URL = (
f'{settings.keycloak_server}/realms/'
f'{settings.keycloak_realm}/protocol/openid-connect/auth'
)
TOKEN_URL = (
f'{settings.keycloak_server}/realms/'
f'{settings.keycloak_realm}/protocol/openid-connect/token'
)
ISSUER = f'{settings.keycloak_server}/realms/{settings.keycloak_realm}'
JWKS_URL = f'{ISSUER}/protocol/openid-connect/certs'
LOGOUT_URL = (
f'{settings.keycloak_server}/realms/'
f'{settings.keycloak_realm}/protocol/openid-connect/logout'
)

View File

@@ -0,0 +1,17 @@
import logging
class ShipmentServiceError(Exception):
def __init__(self, message: str):
super().__init__(message)
logging.error('ShipmentService : %s', message)
class ShipmentNotFoundError(ShipmentServiceError):
pass
class ShipmentCreateError(ShipmentServiceError):
def __init__(self, message: str, field: str | None = None):
super().__init__(message)
self.field = field

View File

@@ -1,46 +1,111 @@
# pylint: disable=E1101
import datetime
import src.messages as messages
import src.shipments.exceptions as exceptions
from sqlmodel import Session, select
import src.models as models
from src import models
def get_all(
session: Session,
names: list[str],
dates: list[str],
forms: list[int]
user: models.User,
names: list[str] = None,
dates: list[str] = None,
forms: list[str] = None
) -> list[models.ShipmentPublic]:
statement = select(models.Shipment)
if len(names) > 0:
statement = (
select(models.Shipment)
.join(
models.Form,
models.Shipment.form_id == models.Form.id)
.join(
models.Productor,
models.Form.productor_id == models.Productor.id)
.where(
models.Productor.type.in_(
[r.name for r in user.roles]
)
)
.distinct()
)
if names and len(names) > 0:
statement = statement.where(models.Shipment.name.in_(names))
if len(dates) > 0:
statement = statement.where(models.Shipment.date.in_(list(map(lambda x: datetime.strptime(x, '%Y-%m-%d'), dates))))
if len(forms) > 0:
statement = statement.join(models.Form).where(models.Form.name.in_(forms))
if dates and len(dates) > 0:
statement = statement.where(
models.Shipment.date.in_(
list(map(
lambda x: datetime.datetime.strptime(
x, '%Y-%m-%d').date(),
dates
))
)
)
if forms and len(forms) > 0:
statement = statement.where(models.Form.name.in_(forms))
return session.exec(statement.order_by(models.Shipment.name)).all()
def get_one(session: Session, shipment_id: int) -> models.ShipmentPublic:
return session.get(models.Shipment, shipment_id)
def create_one(session: Session, shipment: models.ShipmentCreate) -> models.ShipmentPublic:
products = session.exec(select(models.Product).where(models.Product.id.in_(shipment.product_ids))).all()
shipment_create = shipment.model_dump(exclude_unset=True, exclude={'product_ids'})
def create_one(
session: Session,
shipment: models.ShipmentCreate) -> models.ShipmentPublic:
if shipment is None:
raise exceptions.ShipmentCreateError(
messages.Messages.invalid_input(
'shipment', 'input cannot be None'))
products = session.exec(
select(models.Product)
.where(
models.Product.id.in_(
shipment.product_ids
)
)
).all()
shipment_create = shipment.model_dump(
exclude_unset=True, exclude={'product_ids'}
)
new_shipment = models.Shipment(**shipment_create, products=products)
session.add(new_shipment)
session.commit()
session.refresh(new_shipment)
return new_shipment
def update_one(session: Session, id: int, shipment: models.ShipmentUpdate) -> models.ShipmentPublic:
statement = select(models.Shipment).where(models.Shipment.id == id)
def update_one(
session: Session,
_id: int,
shipment: models.ShipmentUpdate) -> models.ShipmentPublic:
if shipment is None:
raise exceptions.ShipmentCreateError(
messages.Messages.invalid_input(
'shipment', 'input cannot be None'))
statement = select(models.Shipment).where(models.Shipment.id == _id)
result = session.exec(statement)
new_shipment = result.first()
if not new_shipment:
return None
raise exceptions.ShipmentNotFoundError(
messages.Messages.not_found('shipment'))
products_to_add = session.exec(select(models.Product).where(models.Product.id.in_(shipment.product_ids))).all()
products_to_add = session.exec(
select(
models.Product
).where(
models.Product.id.in_(
shipment.product_ids
)
)
).all()
new_shipment.products.clear()
for add in products_to_add:
new_shipment.products.append(add)
shipment_updates = shipment.model_dump(exclude_unset=True, exclude={"product_ids"})
shipment_updates = shipment.model_dump(
exclude_unset=True, exclude={"product_ids"}
)
for key, value in shipment_updates.items():
setattr(new_shipment, key, value)
@@ -49,12 +114,15 @@ def update_one(session: Session, id: int, shipment: models.ShipmentUpdate) -> mo
session.refresh(new_shipment)
return new_shipment
def delete_one(session: Session, id: int) -> models.ShipmentPublic:
statement = select(models.Shipment).where(models.Shipment.id == id)
def delete_one(session: Session, _id: int) -> models.ShipmentPublic:
statement = select(models.Shipment).where(models.Shipment.id == _id)
result = session.exec(statement)
shipment = result.first()
if not shipment:
return None
raise exceptions.ShipmentNotFoundError(
messages.Messages.not_found('shipment'))
result = models.ShipmentPublic.model_validate(shipment)
session.delete(shipment)
session.commit()

View File

@@ -1,64 +1,82 @@
from fastapi import APIRouter, HTTPException, Depends, Query
import src.messages as messages
import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.shipments.exceptions as exceptions
import src.shipments.service as service
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session
from src import models
from src.auth.auth import get_current_user
from src.database import get_session
router = APIRouter(prefix='/shipments')
@router.get('/', response_model=list[models.ShipmentPublic], )
@router.get('', response_model=list[models.ShipmentPublic], )
def get_shipments(
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user),
names: list[str] = Query([]),
dates: list[str] = Query([]),
forms: list[str] = Query([]),
):
return service.get_all(
session,
user,
names,
dates,
forms,
)
@router.get('/{id}', response_model=models.ShipmentPublic)
@router.get('/{_id}', response_model=models.ShipmentPublic)
def get_shipment(
id: int,
_id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.get_one(session, id)
result = service.get_one(session, _id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(
status_code=404,
detail=messages.Messages.not_found('shipment')
)
return result
@router.post('/', response_model=models.ShipmentPublic)
@router.post('', response_model=models.ShipmentPublic)
def create_shipment(
shipment: models.ShipmentCreate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.create_one(session, shipment)
try:
result = service.create_one(session, shipment)
except exceptions.ShipmentCreateError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
return result
@router.put('/{id}', response_model=models.ShipmentPublic)
@router.put('/{_id}', response_model=models.ShipmentPublic)
def update_shipment(
id: int, shipment: models.ShipmentUpdate,
_id: int,
shipment: models.ShipmentUpdate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.update_one(session, id, shipment)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
try:
result = service.update_one(session, _id, shipment)
except exceptions.ShipmentNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
return result
@router.delete('/{id}', response_model=models.ShipmentPublic)
@router.delete('/{_id}', response_model=models.ShipmentPublic)
def delete_shipment(
id: int,
_id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
try:
result = service.delete_one(session, _id)
except exceptions.ShipmentNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
return result

View File

@@ -1,14 +1,19 @@
from sqlmodel import Session, select
import src.models as models
from src import models
def get_all(session: Session) -> list[models.TemplatePublic]:
statement = select(models.Template)
return session.exec(statement.order_by(models.Template.name)).all()
def get_one(session: Session, template_id: int) -> models.TemplatePublic:
return session.get(models.Template, template_id)
def create_one(session: Session, template: models.TemplateCreate) -> models.TemplatePublic:
def create_one(
session: Session,
template: models.TemplateCreate) -> models.TemplatePublic:
template_create = template.model_dump(exclude_unset=True)
new_template = models.Template(**template_create)
session.add(new_template)
@@ -16,7 +21,11 @@ def create_one(session: Session, template: models.TemplateCreate) -> models.Temp
session.refresh(new_template)
return new_template
def update_one(session: Session, id: int, template: models.TemplateUpdate) -> models.TemplatePublic:
def update_one(
session: Session,
id: int,
template: models.TemplateUpdate) -> models.TemplatePublic:
statement = select(models.Template).where(models.Template.id == id)
result = session.exec(statement)
new_template = result.first()
@@ -30,6 +39,7 @@ def update_one(session: Session, id: int, template: models.TemplateUpdate) -> mo
session.refresh(new_template)
return new_template
def delete_one(session: Session, id: int) -> models.TemplatePublic:
statement = select(models.Template).where(models.Template.id == id)
result = session.exec(statement)

View File

@@ -1,20 +1,22 @@
from fastapi import APIRouter, HTTPException, Depends
import src.messages as messages
import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.templates.service as service
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session
from src import models
from src.auth.auth import get_current_user
from src.database import get_session
router = APIRouter(prefix='/templates')
@router.get('/', response_model=list[models.TemplatePublic])
@router.get('', response_model=list[models.TemplatePublic])
def get_templates(
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.get_all(session)
@router.get('/{id}', response_model=models.TemplatePublic)
def get_template(
id: int,
@@ -23,10 +25,12 @@ def get_template(
):
result = service.get_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(status_code=404,
detail=messages.Messages.not_found('template'))
return result
@router.post('/', response_model=models.TemplatePublic)
@router.post('', response_model=models.TemplatePublic)
def create_template(
template: models.TemplateCreate,
user: models.User = Depends(get_current_user),
@@ -34,6 +38,7 @@ def create_template(
):
return service.create_one(session, template)
@router.put('/{id}', response_model=models.TemplatePublic)
def update_template(
id: int, template: models.TemplateUpdate,
@@ -42,9 +47,11 @@ def update_template(
):
result = service.update_one(session, id, template)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(status_code=404,
detail=messages.Messages.not_found('template'))
return result
@router.delete('/{id}', response_model=models.TemplatePublic)
def delete_template(
id: int,
@@ -53,5 +60,6 @@ def delete_template(
):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(status_code=404,
detail=messages.Messages.not_found('template'))
return result

View File

@@ -0,0 +1,17 @@
import logging
class UserServiceError(Exception):
def __init__(self, message: str):
super().__init__(message)
logging.error('UserService : %s', message)
class UserNotFoundError(UserServiceError):
pass
class UserCreateError(UserServiceError):
def __init__(self, message: str, field: str | None = None):
super().__init__(message)
self.field = field

View File

@@ -1,5 +1,8 @@
import src.messages as messages
import src.users.exceptions as exceptions
from sqlmodel import Session, select
import src.models as models
from src import models
def get_all(
session: Session,
@@ -13,11 +16,15 @@ def get_all(
statement = statement.where(models.User.email.in_(emails))
return session.exec(statement.order_by(models.User.name)).all()
def get_one(session: Session, user_id: int) -> models.UserPublic:
return session.get(models.User, user_id)
def get_or_create_roles(session: Session, role_names: list[str]) -> list[models.ContractType]:
statement = select(models.ContractType).where(models.ContractType.name.in_(role_names))
def get_or_create_roles(session: Session,
role_names: list[str]) -> list[models.ContractType]:
statement = select(models.ContractType).where(
models.ContractType.name.in_(role_names))
existing = session.exec(statement).all()
existing_roles = {role.name for role in existing}
missing_role = set(role_names) - existing_roles
@@ -33,8 +40,11 @@ def get_or_create_roles(session: Session, role_names: list[str]) -> list[models.
session.refresh(role)
return existing + new_roles
def get_or_create_user(session: Session, user_create: models.UserCreate):
statement = select(models.User).where(models.User.email == user_create.email)
statement = select(
models.User).where(
models.User.email == user_create.email)
user = session.exec(statement).first()
if user:
user_role_names = [r.name for r in user.roles]
@@ -44,11 +54,21 @@ def get_or_create_user(session: Session, user_create: models.UserCreate):
user = create_one(session, user_create)
return user
def get_roles(session: Session):
statement = select(models.ContractType)
statement = (
select(models.ContractType)
)
return session.exec(statement.order_by(models.ContractType.name)).all()
def create_one(session: Session, user: models.UserCreate) -> models.UserPublic:
if user is None:
raise exceptions.UserCreateError(
messages.Messages.invalid_input(
'user', 'input cannot be None'
)
)
new_user = models.User(
name=user.name,
email=user.email
@@ -62,31 +82,39 @@ def create_one(session: Session, user: models.UserCreate) -> models.UserPublic:
session.refresh(new_user)
return new_user
def update_one(session: Session, id: int, user: models.UserCreate) -> models.UserPublic:
statement = select(models.User).where(models.User.id == id)
def update_one(
session: Session,
_id: int,
user: models.UserCreate) -> models.UserPublic:
if user is None:
raise exceptions.UserCreateError(
messages.Messages.invalid_input(
'user', 'input cannot be None'
)
)
statement = select(models.User).where(models.User.id == _id)
result = session.exec(statement)
new_user = result.first()
if not new_user:
return None
user_updates = user.model_dump(exclude="role_names")
for key, value in user_updates.items():
setattr(new_user, key, value)
raise exceptions.UserNotFoundError(f'User {_id} not found')
new_user.email = user.email
new_user.name = user.name
roles = get_or_create_roles(session, user.role_names)
new_user.roles = roles
session.add(new_user)
session.commit()
session.refresh(new_user)
return new_user
def delete_one(session: Session, id: int) -> models.UserPublic:
statement = select(models.User).where(models.User.id == id)
def delete_one(session: Session, _id: int) -> models.UserPublic:
statement = select(models.User).where(models.User.id == _id)
result = session.exec(statement)
user = result.first()
if not user:
return None
raise exceptions.UserNotFoundError(f'User {_id} not found')
result = models.UserPublic.model_validate(user)
session.delete(user)
session.commit()

View File

@@ -1,14 +1,16 @@
from fastapi import APIRouter, HTTPException, Depends, Query
import src.messages as messages
import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.users.exceptions as exceptions
import src.users.service as service
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session
from src import models
from src.auth.auth import get_current_user
from src.database import get_session
router = APIRouter(prefix='/users')
@router.get('/', response_model=list[models.UserPublic])
@router.get('', response_model=list[models.UserPublic])
def get_users(
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user),
@@ -21,6 +23,7 @@ def get_users(
emails,
)
@router.get('/roles', response_model=list[models.ContractType])
def get_roles(
user: models.User = Depends(get_current_user),
@@ -28,44 +31,66 @@ def get_roles(
):
return service.get_roles(session)
@router.get('/{id}', response_model=models.UserPublic)
def get_users(
id: int,
@router.get('/{_id}', response_model=models.UserPublic)
def get_user(
_id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.get_one(session, id)
result = service.get_one(session, _id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(
status_code=404,
detail=messages.Messages.not_found('user')
)
return result
@router.post('/', response_model=models.UserPublic)
@router.post('', response_model=models.UserPublic)
def create_user(
user: models.UserCreate,
logged_user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.create_one(session, user)
try:
user = service.create_one(session, user)
except exceptions.UserCreateError as error:
raise HTTPException(
status_code=400,
detail=str(error)
) from error
return user
@router.put('/{id}', response_model=models.UserPublic)
@router.put('/{_id}', response_model=models.UserPublic)
def update_user(
id: int,
_id: int,
user: models.UserUpdate,
logged_user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.update_one(session, id, user)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
try:
result = service.update_one(session, _id, user)
except exceptions.UserNotFoundError as error:
raise HTTPException(
status_code=404,
detail=messages.Messages.not_found('user')
) from error
return result
@router.delete('/{id}', response_model=models.UserPublic)
def delete_user(
id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
try:
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
except exceptions.UserNotFoundError as error:
raise HTTPException(
status_code=404,
detail=messages.Messages.not_found('user')
) from error
return result

62
backend/tests/conftest.py Normal file
View File

@@ -0,0 +1,62 @@
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.pool import StaticPool
from sqlmodel import Session, SQLModel, create_engine
from src import models
from src.auth.auth import get_current_user
from src.database import get_session
from src.main import app
from .fixtures import *
@pytest.fixture
def mock_session(mocker):
session = mocker.Mock()
def override():
return session
app.dependency_overrides[get_session] = override
yield session
app.dependency_overrides.clear()
@pytest.fixture
def mock_user():
user = models.User(id=1, name='test user', email='test@user.com')
def override():
return user
app.dependency_overrides[get_current_user] = override
yield user
app.dependency_overrides.clear()
@pytest.fixture
def client():
return TestClient(app)
@pytest.fixture(name='session')
def session_fixture():
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
SQLModel.metadata.create_all(engine)
connection = engine.connect()
transaction = connection.begin()
session = Session(bind=connection)
try:
yield session
finally:
transaction.rollback()
session.close()
connection.close()
engine.dispose()

View File

@@ -0,0 +1,65 @@
import tests.factories.contracts as contract_factory
import tests.factories.products as product_factory
from src import models
def contract_product_factory(**kwargs):
contract = contract_factory.contract_factory(id=1)
product = product_factory.product_public_factory(
id=1, type=models.ProductType.RECCURENT)
data = dict(
product_id=1,
shipment_id=1,
quantity=1,
contract_id=1,
product=product,
contract=contract
)
data.update(kwargs)
return models.ContractProduct(**data)
def contract_product_public_factory(**kwargs):
contract = contract_factory.contract_factory(id=1)
product = product_factory.product_public_factory(id=1)
data = dict(
id=1,
product_id=1,
shipment_id=None,
contract=contract,
product=product,
shipment=None,
quantity=1
)
data.update(kwargs)
return models.ContractProductPublic(**data)
def contract_product_create_factory(**kwargs):
data = dict(
product_id=1,
shipment_id=1,
quantity=1,
)
data.update(kwargs)
return models.ContractProductCreate(**data)
def contract_product_update_factory(**kwargs):
data = dict(
product_id=1,
shipment_id=1,
quantity=1,
)
data.update(kwargs)
return models.ContractProductUpdate(**data)
def contract_product_body_factory(**kwargs):
data = dict(
product_id=1,
shipment_id=1,
quantity=1,
)
data.update(kwargs)
return data

View File

@@ -0,0 +1,82 @@
from src import models
from .forms import form_factory
def contract_factory(**kwargs):
data = dict(
id=1,
firstname="test",
lastname="test",
email="test@test.test",
phone="00000000",
payment_method="cheque",
cheque_quantity=1,
form_id=1,
products=[],
cheques=[],
)
data.update(kwargs)
return models.Contract(**data)
def contract_public_factory(**kwargs):
data = dict(
id=1,
firstname="test",
lastname="test",
email="test@test.test",
phone="00000000",
payment_method="cheque",
cheque_quantity=1,
total_price=10,
products=[],
form=form_factory()
)
data.update(kwargs)
return models.ContractPublic(**data)
def contract_create_factory(**kwargs):
data = dict(
firstname="test",
lastname="test",
email="test@test.test",
phone="00000000",
payment_method="cheque",
cheque_quantity=1,
products=[],
cheques=[],
form_id=1,
)
data.update(kwargs)
return models.ContractCreate(**data)
def contract_update_factory(**kwargs):
data = dict(
firstname="test",
lastname="test",
email="test@test.test",
phone="00000000",
payment_method="cheque",
cheque_quantity=1,
)
data.update(kwargs)
return models.ContractUpdate(**data)
def contract_body_factory(**kwargs):
data = dict(
firstname="test",
lastname="test",
email="test@test.test",
phone="00000000",
payment_method="cheque",
cheque_quantity=1,
products=[],
cheques=[],
form_id=1
)
data.update(kwargs)
return data

View File

@@ -0,0 +1,89 @@
import datetime
from src import models
from .productors import productor_public_factory
from .users import user_factory
def form_factory(**kwargs):
data = dict(
id=1,
name='form 1',
productor_id=1,
referer_id=1,
season='hiver-2026',
start=datetime.date(2025, 10, 10),
end=datetime.date(2025, 10, 10),
minimum_shipment_value=0,
visible=True,
referer=user_factory(),
shipments=[],
productor=productor_public_factory(),
)
data.update(kwargs)
return models.Form(**data)
def form_body_factory(**kwargs):
data = dict(
name='form 1',
productor_id=1,
referer_id=1,
season='hiver-2026',
start='2025-10-10',
end='2025-10-10',
minimum_shipment_value=0,
visible=True
)
data.update(kwargs)
return data
def form_create_factory(**kwargs):
data = dict(
name='form 1',
productor_id=1,
referer_id=1,
season='hiver-2026',
start=datetime.date(2025, 10, 10),
end=datetime.date(2025, 10, 10),
minimum_shipment_value=0,
visible=True
)
data.update(kwargs)
return models.FormCreate(**data)
def form_update_factory(**kwargs):
data = dict(
name='form 1',
productor_id=1,
referer_id=1,
season='hiver-2026',
start=datetime.date(2025, 10, 10),
end=datetime.date(2025, 10, 10),
minimum_shipment_value=0,
visible=True
)
data.update(kwargs)
return models.FormUpdate(**data)
def form_public_factory(form=None, shipments=[], **kwargs):
data = dict(
id=1,
name='form 1',
productor_id=1,
referer_id=1,
season='hiver-2026',
start=datetime.date(2025, 10, 10),
end=datetime.date(2025, 10, 10),
minimum_shipment_value=0,
visible=True,
referer=user_factory(),
shipments=[],
productor=productor_public_factory(),
)
data.update(kwargs)
return models.FormPublic(**data)

View File

@@ -0,0 +1,64 @@
from src import models
def productor_factory(**kwargs):
data = dict(
id=1,
name="test productor",
address="test address",
type="test type"
)
data.update(kwargs)
return models.Productor(**data)
def productor_public_factory(**kwargs):
data = dict(
id=1,
name="test productor",
address="test address",
type="test type",
products=[],
payment_methods=[],
)
data.update(kwargs)
return models.ProductorPublic(**data)
def productor_create_factory(**kwargs):
data = dict(
id=1,
name="test productor",
address="test address",
type="test type",
products=[],
payment_methods=[],
)
data.update(kwargs)
return models.ProductorCreate(**data)
def productor_update_factory(**kwargs):
data = dict(
id=1,
name="test productor",
address="test address",
type="test type",
products=[],
payment_methods=[],
)
data.update(kwargs)
return models.ProductorUpdate(**data)
def productor_body_factory(**kwargs):
data = dict(
id=1,
name="test productor",
address="test address",
type="test type",
products=[],
payment_methods=[],
)
data.update(kwargs)
return data

View File

@@ -0,0 +1,68 @@
from src import models
from .productors import productor_factory
def product_body_factory(**kwargs):
data = dict(
name='product test 1',
unit=models.Unit.PIECE,
price=10.2,
price_kg=20.4,
quantity=500,
quantity_unit='g',
type=models.ProductType.OCCASIONAL,
productor_id=1,
)
data.update(kwargs)
return data
def product_create_factory(**kwargs):
data = dict(
name='product test 1',
unit=models.Unit.PIECE,
price=10.2,
price_kg=20.4,
quantity=500,
quantity_unit='g',
type=models.ProductType.OCCASIONAL,
productor_id=1,
)
data.update(kwargs)
return models.ProductCreate(**data)
def product_update_factory(**kwargs):
data = dict(
name='product test 1',
unit=models.Unit.PIECE,
price=10.2,
price_kg=20.4,
quantity=500,
quantity_unit='g',
type=models.ProductType.OCCASIONAL,
productor_id=1,
)
data.update(kwargs)
return models.ProductUpdate(**data)
def product_public_factory(productor=None, shipments=[], **kwargs):
if productor is None:
productor = productor_factory()
data = dict(
id=1,
name='product test 1',
unit=models.Unit.PIECE,
price=10.2,
price_kg=20.4,
quantity=500,
quantity_unit='g',
type=models.ProductType.OCCASIONAL,
productor_id=1,
productor=productor,
shipments=shipments,
)
data.update(kwargs)
return models.ProductPublic(**data)

View File

@@ -0,0 +1,59 @@
import datetime
from src import models
def shipment_factory(**kwargs):
data = dict(
id=1,
name="test shipment",
date=datetime.date(2025, 10, 10),
form_id=1,
)
data.update(kwargs)
return models.Shipment(**data)
def shipment_public_factory(**kwargs):
data = dict(
id=1,
name="test shipment",
date=datetime.date(2025, 10, 10),
form_id=1,
products=[],
form=models.Form(id=1, name="test")
)
data.update(kwargs)
return models.ShipmentPublic(**data)
def shipment_create_factory(**kwargs):
data = dict(
name="test shipment",
form_id=1,
date='2025-10-10',
product_ids=[],
)
data.update(kwargs)
return models.ShipmentCreate(**data)
def shipment_update_factory(**kwargs):
data = dict(
name="test shipment",
form_id=1,
date='2025-10-10',
product_ids=[],
)
data.update(kwargs)
return models.ShipmentUpdate(**data)
def shipment_body_factory(**kwargs):
data = dict(
name="test shipment",
form_id=1,
date="2025-10-10",
)
data.update(kwargs)
return data

View File

@@ -0,0 +1,53 @@
from src import models
def user_factory(**kwargs):
data = dict(
id=1,
name="test user",
email="test.test@test.test",
roles=[]
)
data.update(kwargs)
return models.User(**data)
def user_public_factory(**kwargs):
data = dict(
id=1,
name="test user",
email="test.test@test.test",
roles=[]
)
data.update(kwargs)
return models.UserPublic(**data)
def user_create_factory(**kwargs):
data = dict(
name="test user",
email="test.test@test.test",
role_names=[],
)
data.update(kwargs)
return models.UserCreate(**data)
def user_update_factory(**kwargs):
data = dict(
name="test user",
email="test.test@test.test",
role_names=[],
)
data.update(kwargs)
return models.UserUpdate(**data)
def user_body_factory(**kwargs):
data = dict(
name="test user",
email="test.test@test.test",
role_names=[],
)
data.update(kwargs)
return data

184
backend/tests/fixtures.py Normal file
View File

@@ -0,0 +1,184 @@
import datetime
import pytest
import src.forms.service as forms_service
import src.productors.service as productors_service
import src.products.service as products_service
import src.shipments.service as shipments_service
import src.users.service as users_service
import tests.factories.forms as forms_factory
import tests.factories.productors as productors_factory
import tests.factories.products as products_factory
import tests.factories.shipments as shipments_factory
import tests.factories.users as users_factory
from sqlmodel import Session
from src import models
@pytest.fixture
def productor(session: Session) -> models.ProductorPublic:
productor = productors_service.create_one(
session,
productors_factory.productor_create_factory(
name='test productor',
type='Légumineuses',
)
)
return productor
@pytest.fixture
def productors(session: Session) -> models.ProductorPublic:
productors = [
productors_service.create_one(
session,
productors_factory.productor_create_factory(
name='test productor 1',
type='Légumineuses',
)
),
productors_service.create_one(
session,
productors_factory.productor_create_factory(
name='test productor 2',
type='Légumes',
)
)
]
return productors
@pytest.fixture
def products(session: Session,
productor: models.ProductorPublic) -> list[models.ProductPublic]:
products = [
products_service.create_one(
session,
products_factory.product_create_factory(
name='product 1 occasionnal',
type=models.ProductType.OCCASIONAL,
productor_id=productor.id
)
),
products_service.create_one(
session,
products_factory.product_create_factory(
name='product 2 recurrent',
type=models.ProductType.RECCURENT,
productor_id=productor.id
)
),
]
return products
@pytest.fixture
def user(session: Session) -> models.UserPublic:
user = users_service.create_one(
session,
users_factory.user_create_factory(
name='test user',
email='test@test.com',
role_names=['Légumineuses']
)
)
return user
@pytest.fixture
def users(session: Session) -> list[models.UserPublic]:
users = [
users_service.create_one(
session,
users_factory.user_create_factory(
name='test user 1 (admin)',
email='test1@test.com',
role_names=[
'Légumineuses',
'Légumes',
'Oeufs',
'Porc-Agneau',
'Vin',
'Fruits'])),
users_service.create_one(
session,
users_factory.user_create_factory(
name='test user 2',
email='test2@test.com',
role_names=['Légumineuses'])),
users_service.create_one(
session,
users_factory.user_create_factory(
name='test user 3',
email='test3@test.com',
role_names=['Porc-Agneau']))]
return users
@pytest.fixture
def referer(session: Session) -> models.UserPublic:
referer = users_service.create_one(
session,
users_factory.user_create_factory(
name='test referer',
email='test@test.com',
role_names=['Légumineuses'],
)
)
return referer
@pytest.fixture
def shipments(session: Session,
forms: list[models.FormPublic],
products: list[models.ProductPublic]):
shipments = [
shipments_service.create_one(
session,
shipments_factory.shipment_create_factory(
name='test shipment 1',
date=datetime.date(2025, 10, 10),
form_id=forms[0].id,
product_ids=[p.id for p in products]
)
),
shipments_service.create_one(
session,
shipments_factory.shipment_create_factory(
name='test shipment 2',
date=datetime.date(2025, 11, 10),
form_id=forms[0].id,
product_ids=[p.id for p in products]
)
),
]
return shipments
@pytest.fixture
def forms(
session: Session,
productor: models.ProductorPublic,
referer: models.UserPublic
) -> list[models.FormPublic]:
forms = [
forms_service.create_one(
session,
forms_factory.form_create_factory(
name='test form 1',
productor_id=productor.id,
referer_id=referer.id,
season='test season 1',
)
),
forms_service.create_one(
session,
forms_factory.form_create_factory(
name='test form 2',
productor_id=productor.id,
referer_id=referer.id,
season='test season 2',
)
)
]
return forms

View File

@@ -0,0 +1,207 @@
import src.contracts.service as service
import tests.factories.contract_products as contract_products_factory
import tests.factories.contracts as contract_factory
import tests.factories.forms as form_factory
from fastapi.exceptions import HTTPException
from src import models
from src.auth.auth import get_current_user
from src.main import app
class TestContracts:
def test_get_all(self, client, mocker, mock_session, mock_user):
mock_results = [
contract_factory.contract_public_factory(id=1),
contract_factory.contract_public_factory(id=2),
contract_factory.contract_public_factory(id=3),
]
mock = mocker.patch.object(
service,
'get_all',
return_value=mock_results
)
response = client.get('/api/contracts')
response_data = response.json()
assert response.status_code == 200
assert response_data[0]['id'] == 1
assert len(response_data) == len(mock_results)
mock.assert_called_once_with(
mock_session,
mock_user,
[],
)
def test_get_all_filters(self, client, mocker, mock_session, mock_user):
mock_results = [
contract_factory.contract_public_factory(id=2),
]
mock = mocker.patch.object(
service,
'get_all',
return_value=mock_results
)
response = client.get('/api/contracts?forms=form test')
response_data = response.json()
assert response.status_code == 200
assert response_data[0]['id'] == 2
assert len(response_data) == len(mock_results)
mock.assert_called_once_with(
mock_session,
mock_user,
['form test'],
)
def test_get_all_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.contracts.service.get_all')
response = client.get('/api/contracts')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_get_one(self, client, mocker, mock_session, mock_user):
mock_result = contract_factory.contract_public_factory(id=2)
mock = mocker.patch.object(
service,
'get_one',
return_value=mock_result
)
mocker.patch.object(
service,
'is_allowed',
return_value=True
)
response = client.get('/api/contracts/2')
response_data = response.json()
assert response.status_code == 200
assert response_data['id'] == 2
mock.assert_called_once_with(
mock_session,
2
)
def test_get_one_notfound(self, client, mocker, mock_session, mock_user):
mock_result = None
mock = mocker.patch.object(
service,
'get_one',
return_value=mock_result
)
mocker.patch.object(
service,
'is_allowed',
return_value=True
)
response = client.get('/api/contracts/2')
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2
)
def test_get_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.contracts.service.get_one')
response = client.get('/api/contracts/2')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_delete_one(self, client, mocker, mock_session, mock_user):
contract_result = contract_factory.contract_public_factory()
mock = mocker.patch.object(
service,
'delete_one',
return_value=contract_result
)
mocker.patch.object(
service,
'is_allowed',
return_value=True
)
response = client.delete('/api/contracts/2')
assert response.status_code == 200
mock.assert_called_once_with(
mock_session,
2,
)
def test_delete_one_notfound(
self,
client,
mocker,
mock_session,
mock_user
):
contract_result = None
mock = mocker.patch.object(
service,
'delete_one',
return_value=contract_result
)
mocker.patch.object(
service,
'is_allowed',
return_value=True
)
response = client.delete('/api/contracts/2')
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2,
)
def test_delete_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user
):
def unauthorized():
raise HTTPException(status_code=401)
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.contracts.service.delete_one')
response = client.delete('/api/contracts/2')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()

View File

@@ -0,0 +1,355 @@
import src.forms.exceptions as forms_exceptions
import src.forms.service as service
import src.messages as messages
import tests.factories.forms as form_factory
from fastapi.exceptions import HTTPException
from src import models
from src.auth.auth import get_current_user
from src.main import app
class TestForms:
def test_get_all(self, client, mocker, mock_session, mock_user):
mock_results = [
form_factory.form_public_factory(name="test 1", id=1),
form_factory.form_public_factory(name="test 2", id=2),
form_factory.form_public_factory(name="test 3", id=3),
]
mock = mocker.patch.object(
service,
'get_all',
return_value=mock_results
)
response = client.get('/api/forms/referents')
response_data = response.json()
assert response.status_code == 200
assert response_data[0]['id'] == 1
assert len(response_data) == len(mock_results)
mock.assert_called_once_with(
mock_session,
[],
[],
False,
mock_user,
)
def test_get_all_filters(self, client, mocker, mock_session, mock_user):
mock_results = [
form_factory.form_public_factory(name="test 2", id=2),
]
mock = mocker.patch.object(
service,
'get_all',
return_value=mock_results
)
response = client.get(
'/api/forms/referents?current_season=true&seasons=hiver-2025&productors=test productor')
response_data = response.json()
assert response.status_code == 200
assert response_data[0]['id'] == 2
assert len(response_data) == len(mock_results)
mock.assert_called_once_with(
mock_session,
['hiver-2025'],
['test productor'],
True,
mock_user,
)
def test_get_all_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.forms.service.get_all')
response = client.get('/api/forms/referents')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_get_one(self, client, mocker, mock_session, mock_user):
mock_result = form_factory.form_public_factory(name="test 2", id=2)
mock = mocker.patch.object(
service,
'get_one',
return_value=mock_result
)
response = client.get('/api/forms/2')
response_data = response.json()
assert response.status_code == 200
assert response_data['id'] == 2
mock.assert_called_once_with(
mock_session,
2
)
def test_get_one_notfound(self, client, mocker, mock_session, mock_user):
mock_result = None
mock = mocker.patch.object(
service,
'get_one',
return_value=mock_result
)
response = client.get('/api/forms/2')
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2
)
def test_create_one(self, client, mocker, mock_session, mock_user):
form_body = form_factory.form_body_factory(name='test form create')
form_create = form_factory.form_create_factory(name='test form create')
form_result = form_factory.form_public_factory(name='test form create')
mock = mocker.patch.object(
service,
'create_one',
return_value=form_result
)
response = client.post('/api/forms', json=form_body)
response_data = response.json()
assert response.status_code == 200
assert response_data['name'] == 'test form create'
mock.assert_called_once_with(
mock_session,
form_create
)
def test_create_one_referer_notfound(
self, client, mocker, mock_session, mock_user):
form_body = form_factory.form_body_factory(
name='test form create', referer_id=12312)
form_create = form_factory.form_create_factory(
name='test form create', referer_id=12312)
mock = mocker.patch.object(
service, 'create_one', side_effect=forms_exceptions.UserNotFoundError(
messages.Messages.not_found('referer')))
response = client.post('/api/forms', json=form_body)
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
form_create
)
def test_create_one_productor_notfound(
self, client, mocker, mock_session, mock_user):
form_body = form_factory.form_body_factory(
name='test form create', productor_id=1231)
form_create = form_factory.form_create_factory(
name='test form create', productor_id=1231)
mock = mocker.patch.object(
service, 'create_one', side_effect=forms_exceptions.ProductorNotFoundError(
messages.Messages.not_found('productor')))
response = client.post('/api/forms', json=form_body)
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
form_create
)
def test_create_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
form_body = form_factory.form_body_factory(name='test form create')
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.forms.service.create_one')
response = client.post('/api/forms', json=form_body)
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_update_one(self, client, mocker, mock_session, mock_user):
form_body = form_factory.form_body_factory(name='test form update')
form_update = form_factory.form_update_factory(name='test form update')
form_result = form_factory.form_public_factory(name='test form update')
mock = mocker.patch.object(
service,
'update_one',
return_value=form_result
)
response = client.put('/api/forms/2', json=form_body)
response_data = response.json()
assert response.status_code == 200
assert response_data['name'] == 'test form update'
mock.assert_called_once_with(
mock_session,
2,
form_update
)
def test_update_one_notfound(
self,
client,
mocker,
mock_session,
mock_user):
form_body = form_factory.form_body_factory(name='test form update')
form_update = form_factory.form_update_factory(name='test form update')
mock = mocker.patch.object(
service, 'update_one', side_effect=forms_exceptions.FormNotFoundError(
messages.Messages.not_found('form')))
response = client.put('/api/forms/2', json=form_body)
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2,
form_update
)
def test_update_one_referer_notfound(
self, client, mocker, mock_session, mock_user):
form_body = form_factory.form_body_factory(name='test form update')
form_update = form_factory.form_update_factory(name='test form update')
mock = mocker.patch.object(
service, 'update_one', side_effect=forms_exceptions.UserNotFoundError(
messages.Messages.not_found('referer')))
response = client.put('/api/forms/2', json=form_body)
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2,
form_update
)
def test_update_one_productor_notfound(
self, client, mocker, mock_session, mock_user):
form_body = form_factory.form_body_factory(name='test form update')
form_update = form_factory.form_update_factory(name='test form update')
mock = mocker.patch.object(
service, 'update_one', side_effect=forms_exceptions.ProductorNotFoundError(
messages.Messages.not_found('productor')))
response = client.put('/api/forms/2', json=form_body)
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2,
form_update
)
def test_update_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
form_body = form_factory.form_body_factory(name='test form update')
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.forms.service.update_one')
response = client.put('/api/forms/2', json=form_body)
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_delete_one(self, client, mocker, mock_session, mock_user):
form_result = form_factory.form_public_factory(name='test form delete')
mock = mocker.patch.object(
service,
'delete_one',
return_value=form_result
)
response = client.delete('/api/forms/2')
response_data = response.json()
assert response.status_code == 200
assert response_data['name'] == 'test form delete'
mock.assert_called_once_with(
mock_session,
2,
)
def test_delete_one_notfound(
self,
client,
mocker,
mock_session,
mock_user):
form_result = None
mock = mocker.patch.object(
service, 'delete_one', side_effect=forms_exceptions.FormNotFoundError(
messages.Messages.not_found('form')))
response = client.delete('/api/forms/2')
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2,
)
def test_delete_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.forms.service.delete_one')
response = client.delete('/api/forms/2')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()

View File

@@ -0,0 +1,309 @@
import src.messages as messages
import src.productors.exceptions as exceptions
import src.productors.service as service
import tests.factories.productors as productor_factory
from fastapi.exceptions import HTTPException
from src import models
from src.auth.auth import get_current_user
from src.main import app
class TestProductors:
def test_get_all(self, client, mocker, mock_session, mock_user):
mock_results = [
productor_factory.productor_public_factory(name="test 1", id=1),
productor_factory.productor_public_factory(name="test 2", id=2),
productor_factory.productor_public_factory(name="test 3", id=3),
]
mock = mocker.patch.object(
service,
'get_all',
return_value=mock_results
)
response = client.get('/api/productors')
response_data = response.json()
assert response.status_code == 200
assert response_data[0]['id'] == 1
assert len(response_data) == len(mock_results)
mock.assert_called_once_with(
mock_session,
mock_user,
[],
[],
)
def test_get_all_filters(self, client, mocker, mock_session, mock_user):
mock_results = [
productor_factory.productor_public_factory(name="test 2", id=2),
]
mock = mocker.patch.object(
service,
'get_all',
return_value=mock_results
)
response = client.get(
'/api/productors?types=Légumineuses&names=test 2')
response_data = response.json()
assert response.status_code == 200
assert response_data[0]['id'] == 2
assert len(response_data) == len(mock_results)
mock.assert_called_once_with(
mock_session,
mock_user,
['test 2'],
['Légumineuses'],
)
def test_get_all_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.productors.service.get_all')
response = client.get('/api/productors')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_get_one(self, client, mocker, mock_session, mock_user):
mock_result = productor_factory.productor_public_factory(
name="test 2", id=2)
mock = mocker.patch.object(
service,
'get_one',
return_value=mock_result
)
response = client.get('/api/productors/2')
response_data = response.json()
assert response.status_code == 200
assert response_data['id'] == 2
mock.assert_called_once_with(
mock_session,
2
)
def test_get_one_notfound(self, client, mocker, mock_session, mock_user):
mock_result = None
mock = mocker.patch.object(
service,
'get_one',
return_value=mock_result
)
response = client.get('/api/productors/2')
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2
)
def test_get_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.productors.service.get_one')
response = client.get('/api/productors/2')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_create_one(self, client, mocker, mock_session, mock_user):
productor_body = productor_factory.productor_body_factory(
name='test productor create')
productor_create = productor_factory.productor_create_factory(
name='test productor create')
productor_result = productor_factory.productor_public_factory(
name='test productor create')
mock = mocker.patch.object(
service,
'create_one',
return_value=productor_result
)
response = client.post('/api/productors', json=productor_body)
response_data = response.json()
assert response.status_code == 200
assert response_data['name'] == 'test productor create'
mock.assert_called_once_with(
mock_session,
productor_create
)
def test_create_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
productor_body = productor_factory.productor_body_factory(
name='test productor create')
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.productors.service.create_one')
response = client.post('/api/productors', json=productor_body)
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_update_one(self, client, mocker, mock_session, mock_user):
productor_body = productor_factory.productor_body_factory(
name='test productor update')
productor_update = productor_factory.productor_update_factory(
name='test productor update')
productor_result = productor_factory.productor_public_factory(
name='test productor update')
mock = mocker.patch.object(
service,
'update_one',
return_value=productor_result
)
response = client.put('/api/productors/2', json=productor_body)
response_data = response.json()
assert response.status_code == 200
assert response_data['name'] == 'test productor update'
mock.assert_called_once_with(
mock_session,
2,
productor_update
)
def test_update_one_notfound(
self,
client,
mocker,
mock_session,
mock_user):
productor_body = productor_factory.productor_body_factory(
name='test productor update')
productor_update = productor_factory.productor_update_factory(
name='test productor update')
productor_result = None
mock = mocker.patch.object(
service, 'update_one', side_effect=exceptions.ProductorNotFoundError(
messages.Messages.not_found('productor')))
response = client.put('/api/productors/2', json=productor_body)
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2,
productor_update
)
def test_update_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
productor_body = productor_factory.productor_body_factory(
name='test productor update')
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.productors.service.update_one')
response = client.put('/api/productors/2', json=productor_body)
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_delete_one(self, client, mocker, mock_session, mock_user):
productor_result = productor_factory.productor_public_factory(
name='test productor delete')
mock = mocker.patch.object(
service,
'delete_one',
return_value=productor_result
)
response = client.delete('/api/productors/2')
response_data = response.json()
assert response.status_code == 200
assert response_data['name'] == 'test productor delete'
mock.assert_called_once_with(
mock_session,
2,
)
def test_delete_one_notfound(
self,
client,
mocker,
mock_session,
mock_user):
productor_result = None
mock = mocker.patch.object(
service, 'delete_one', side_effect=exceptions.ProductorNotFoundError(
messages.Messages.not_found('productor')))
response = client.delete('/api/productors/2')
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2,
)
def test_delete_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
productor_body = productor_factory.productor_body_factory(
name='test productor delete')
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.productors.service.delete_one')
response = client.delete('/api/productors/2')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()

View File

@@ -0,0 +1,313 @@
import src.products.exceptions as exceptions
import src.products.service as service
import tests.factories.products as product_factory
from fastapi.exceptions import HTTPException
from src import models
from src.auth.auth import get_current_user
from src.main import app
class TestProducts:
def test_get_all(self, client, mocker, mock_session, mock_user):
mock_results = [
product_factory.product_public_factory(name="test 1", id=1),
product_factory.product_public_factory(name="test 2", id=2),
product_factory.product_public_factory(name="test 3", id=3),
]
mock = mocker.patch.object(
service,
'get_all',
return_value=mock_results
)
response = client.get('/api/products')
response_data = response.json()
assert response.status_code == 200
assert response_data[0]['id'] == 1
assert len(response_data) == len(mock_results)
mock.assert_called_once_with(
mock_session,
mock_user,
[],
[],
[]
)
def test_get_all_filters(self, client, mocker, mock_session, mock_user):
mock_results = [
product_factory.product_public_factory(name="test 2", id=2),
]
mock = mocker.patch.object(
service,
'get_all',
return_value=mock_results
)
response = client.get('/api/products?types=1&names=test 2')
response_data = response.json()
assert response.status_code == 200
assert response_data[0]['id'] == 2
assert len(response_data) == len(mock_results)
mock.assert_called_once_with(
mock_session,
mock_user,
['test 2'],
[],
['1'],
)
def test_get_all_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.products.service.get_all')
response = client.get('/api/products')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_get_one(self, client, mocker, mock_session, mock_user):
mock_result = product_factory.product_public_factory(
name="test 2", id=2)
mock = mocker.patch.object(
service,
'get_one',
return_value=mock_result
)
response = client.get('/api/products/2')
response_data = response.json()
assert response.status_code == 200
assert response_data['id'] == 2
mock.assert_called_once_with(
mock_session,
2
)
def test_get_one_notfound(self, client, mocker, mock_session, mock_user):
mock_result = None
mock = mocker.patch.object(
service,
'get_one',
return_value=mock_result
)
response = client.get('/api/products/2')
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2
)
def test_get_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.products.service.get_one')
response = client.get('/api/products/2')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_create_one(self, client, mocker, mock_session, mock_user):
product_body = product_factory.product_body_factory(
name='test product create')
product_create = product_factory.product_create_factory(
name='test product create')
product_result = product_factory.product_public_factory(
name='test product create')
mock = mocker.patch.object(
service,
'create_one',
return_value=product_result
)
response = client.post('/api/products', json=product_body)
response_data = response.json()
assert response.status_code == 200
assert response_data['name'] == 'test product create'
mock.assert_called_once_with(
mock_session,
product_create
)
def test_create_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
product_body = product_factory.product_body_factory(
name='test product create')
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.products.service.create_one')
response = client.post('/api/products', json=product_body)
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_update_one(self, client, mocker, mock_session, mock_user):
product_body = product_factory.product_body_factory(
name='test product update')
product_update = product_factory.product_update_factory(
name='test product update')
product_result = product_factory.product_public_factory(
name='test product update')
mock = mocker.patch.object(
service,
'update_one',
return_value=product_result
)
response = client.put('/api/products/2', json=product_body)
response_data = response.json()
assert response.status_code == 200
assert response_data['name'] == 'test product update'
mock.assert_called_once_with(
mock_session,
2,
product_update
)
def test_update_one_notfound(
self,
client,
mocker,
mock_session,
mock_user):
product_body = product_factory.product_body_factory(
name='test product update')
product_update = product_factory.product_update_factory(
name='test product update')
product_result = None
mock = mocker.patch.object(
service,
'update_one',
side_effect=exceptions.ProductNotFoundError('Product not found')
)
response = client.put('/api/products/2', json=product_body)
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2,
product_update
)
def test_update_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
product_body = product_factory.product_body_factory(
name='test product update')
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.products.service.update_one')
response = client.put('/api/products/2', json=product_body)
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_delete_one(self, client, mocker, mock_session, mock_user):
product_result = product_factory.product_public_factory(
name='test product delete')
mock = mocker.patch.object(
service,
'delete_one',
return_value=product_result
)
response = client.delete('/api/products/2')
response_data = response.json()
assert response.status_code == 200
assert response_data['name'] == 'test product delete'
mock.assert_called_once_with(
mock_session,
2,
)
def test_delete_one_notfound(
self,
client,
mocker,
mock_session,
mock_user):
product_result = None
mock = mocker.patch.object(
service,
'delete_one',
side_effect=exceptions.ProductNotFoundError('Product not found')
)
response = client.delete('/api/products/2')
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2,
)
def test_delete_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
product_body = product_factory.product_body_factory(
name='test product delete')
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.products.service.delete_one')
response = client.delete('/api/products/2')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()

View File

@@ -0,0 +1,310 @@
import src.messages as messages
import src.shipments.exceptions as exceptions
import src.shipments.service as service
import tests.factories.shipments as shipment_factory
from fastapi.exceptions import HTTPException
from src import models
from src.auth.auth import get_current_user
from src.main import app
class TestShipments:
def test_get_all(self, client, mocker, mock_session, mock_user):
mock_results = [
shipment_factory.shipment_public_factory(name="test 1", id=1),
shipment_factory.shipment_public_factory(name="test 2", id=2),
shipment_factory.shipment_public_factory(name="test 3", id=3),
]
mock = mocker.patch.object(
service,
'get_all',
return_value=mock_results
)
response = client.get('/api/shipments')
response_data = response.json()
assert response.status_code == 200
assert response_data[0]['id'] == 1
assert len(response_data) == len(mock_results)
mock.assert_called_once_with(
mock_session,
mock_user,
[],
[],
[],
)
def test_get_all_filters(self, client, mocker, mock_session, mock_user):
mock_results = [
shipment_factory.shipment_public_factory(name="test 2", id=2),
]
mock = mocker.patch.object(
service,
'get_all',
return_value=mock_results
)
response = client.get(
'/api/shipments?dates=2025-10-10&names=test 2&forms=contract form 1')
response_data = response.json()
assert response.status_code == 200
assert response_data[0]['id'] == 2
assert len(response_data) == len(mock_results)
mock.assert_called_once_with(
mock_session,
mock_user,
['test 2'],
['2025-10-10'],
['contract form 1'],
)
def test_get_all_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.shipments.service.get_all')
response = client.get('/api/shipments')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_get_one(self, client, mocker, mock_session, mock_user):
mock_result = shipment_factory.shipment_public_factory(
name="test 2", id=2)
mock = mocker.patch.object(
service,
'get_one',
return_value=mock_result
)
response = client.get('/api/shipments/2')
response_data = response.json()
assert response.status_code == 200
assert response_data['id'] == 2
mock.assert_called_once_with(
mock_session,
2
)
def test_get_one_notfound(self, client, mocker, mock_session, mock_user):
mock_result = None
mock = mocker.patch.object(
service,
'get_one',
return_value=mock_result
)
response = client.get('/api/shipments/2')
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2
)
def test_get_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.shipments.service.get_one')
response = client.get('/api/shipments/2')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_create_one(self, client, mocker, mock_session, mock_user):
shipment_body = shipment_factory.shipment_body_factory(
name='test shipment create')
shipment_create = shipment_factory.shipment_create_factory(
name='test shipment create')
shipment_result = shipment_factory.shipment_public_factory(
name='test shipment create')
mock = mocker.patch.object(
service,
'create_one',
return_value=shipment_result
)
response = client.post('/api/shipments', json=shipment_body)
response_data = response.json()
assert response.status_code == 200
assert response_data['name'] == 'test shipment create'
mock.assert_called_once_with(
mock_session,
shipment_create
)
def test_create_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
shipment_body = shipment_factory.shipment_body_factory(
name='test shipment create')
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.shipments.service.create_one')
response = client.post('/api/shipments', json=shipment_body)
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_update_one(self, client, mocker, mock_session, mock_user):
shipment_body = shipment_factory.shipment_body_factory(
name='test shipment update')
shipment_update = shipment_factory.shipment_update_factory(
name='test shipment update')
shipment_result = shipment_factory.shipment_public_factory(
name='test shipment update')
mock = mocker.patch.object(
service,
'update_one',
return_value=shipment_result
)
response = client.put('/api/shipments/2', json=shipment_body)
response_data = response.json()
assert response.status_code == 200
assert response_data['name'] == 'test shipment update'
mock.assert_called_once_with(
mock_session,
2,
shipment_update
)
def test_update_one_notfound(
self,
client,
mocker,
mock_session,
mock_user):
shipment_body = shipment_factory.shipment_body_factory(
name='test shipment update')
shipment_update = shipment_factory.shipment_update_factory(
name='test shipment update')
mock = mocker.patch.object(
service, 'update_one', side_effect=exceptions.ShipmentNotFoundError(
messages.Messages.not_found('shipment')))
response = client.put('/api/shipments/2', json=shipment_body)
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2,
shipment_update
)
def test_update_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
shipment_body = shipment_factory.shipment_body_factory(
name='test shipment update')
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.shipments.service.update_one')
response = client.put('/api/shipments/2', json=shipment_body)
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_delete_one(self, client, mocker, mock_session, mock_user):
shipment_result = shipment_factory.shipment_public_factory(
name='test shipment delete')
mock = mocker.patch.object(
service,
'delete_one',
return_value=shipment_result
)
response = client.delete('/api/shipments/2')
response_data = response.json()
assert response.status_code == 200
assert response_data['name'] == 'test shipment delete'
mock.assert_called_once_with(
mock_session,
2,
)
def test_delete_one_notfound(
self,
client,
mocker,
mock_session,
mock_user):
shipment_result = None
mock = mocker.patch.object(
service, 'delete_one', side_effect=exceptions.ShipmentNotFoundError(
messages.Messages.not_found('shipment')))
response = client.delete('/api/shipments/2')
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2,
)
def test_delete_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
shipment_body = shipment_factory.shipment_body_factory(
name='test shipment delete')
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.shipments.service.delete_one')
response = client.delete('/api/shipments/2')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()

View File

@@ -0,0 +1,296 @@
import src.users.exceptions as exceptions
import src.users.service as service
import tests.factories.users as user_factory
from fastapi.exceptions import HTTPException
from src import models
from src.auth.auth import get_current_user
from src.main import app
class TestUsers:
def test_get_all(self, client, mocker, mock_session, mock_user):
mock_results = [
user_factory.user_public_factory(name="test 1", id=1),
user_factory.user_public_factory(name="test 2", id=2),
user_factory.user_public_factory(name="test 3", id=3),
]
mock = mocker.patch.object(
service,
'get_all',
return_value=mock_results
)
response = client.get('/api/users')
response_data = response.json()
assert response.status_code == 200
assert response_data[0]['id'] == 1
assert len(response_data) == len(mock_results)
mock.assert_called_once_with(
mock_session,
[],
[],
)
def test_get_all_filters(self, client, mocker, mock_session, mock_user):
mock_results = [
user_factory.user_public_factory(name="test 2", id=2),
]
mock = mocker.patch.object(
service,
'get_all',
return_value=mock_results
)
response = client.get('/api/users?emails=test@test.test&names=test 2')
response_data = response.json()
assert response.status_code == 200
assert response_data[0]['id'] == 2
assert len(response_data) == len(mock_results)
mock.assert_called_once_with(
mock_session,
['test 2'],
['test@test.test'],
)
def test_get_all_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.users.service.get_all')
response = client.get('/api/users')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_get_one(self, client, mocker, mock_session, mock_user):
mock_result = user_factory.user_public_factory(name="test 2", id=2)
mock = mocker.patch.object(
service,
'get_one',
return_value=mock_result
)
response = client.get('/api/users/2')
response_data = response.json()
assert response.status_code == 200
assert response_data['id'] == 2
mock.assert_called_once_with(
mock_session,
2
)
def test_get_one_notfound(self, client, mocker, mock_session, mock_user):
mock_result = None
mock = mocker.patch.object(
service,
'get_one',
return_value=mock_result
)
response = client.get('/api/users/2')
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2
)
def test_get_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.users.service.get_one')
response = client.get('/api/users/2')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_create_one(self, client, mocker, mock_session, mock_user):
user_body = user_factory.user_body_factory(name='test user create')
user_create = user_factory.user_create_factory(name='test user create')
user_result = user_factory.user_public_factory(name='test user create')
mock = mocker.patch.object(
service,
'create_one',
return_value=user_result
)
response = client.post('/api/users', json=user_body)
response_data = response.json()
assert response.status_code == 200
assert response_data['name'] == 'test user create'
mock.assert_called_once_with(
mock_session,
user_create
)
def test_create_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
user_body = user_factory.user_body_factory(name='test user create')
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.users.service.create_one')
response = client.post('/api/users', json=user_body)
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_update_one(self, client, mocker, mock_session, mock_user):
user_body = user_factory.user_body_factory(name='test user update')
user_update = user_factory.user_update_factory(name='test user update')
user_result = user_factory.user_public_factory(name='test user update')
mock = mocker.patch.object(
service,
'update_one',
return_value=user_result
)
response = client.put('/api/users/2', json=user_body)
response_data = response.json()
assert response.status_code == 200
assert response_data['name'] == 'test user update'
mock.assert_called_once_with(
mock_session,
2,
user_update
)
def test_update_one_notfound(
self,
client,
mocker,
mock_session,
mock_user):
user_body = user_factory.user_body_factory(name='test user update')
user_update = user_factory.user_update_factory(name='test user update')
user_result = None
mock = mocker.patch.object(
service,
'update_one',
side_effect=exceptions.UserNotFoundError('User 2 not found')
)
response = client.put('/api/users/2', json=user_body)
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2,
user_update
)
def test_update_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
user_body = user_factory.user_body_factory(name='test user update')
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.users.service.update_one')
response = client.put('/api/users/2', json=user_body)
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()
def test_delete_one(self, client, mocker, mock_session, mock_user):
user_result = user_factory.user_public_factory(name='test user delete')
mock = mocker.patch.object(
service,
'delete_one',
return_value=user_result
)
response = client.delete('/api/users/2')
response_data = response.json()
assert response.status_code == 200
assert response_data['name'] == 'test user delete'
mock.assert_called_once_with(
mock_session,
2,
)
def test_delete_one_notfound(
self,
client,
mocker,
mock_session,
mock_user):
user_result = None
mock = mocker.patch.object(
service,
'delete_one',
side_effect=exceptions.UserNotFoundError('User 2 not found')
)
response = client.delete('/api/users/2')
response_data = response.json()
assert response.status_code == 404
mock.assert_called_once_with(
mock_session,
2,
)
def test_delete_one_unauthorized(
self,
client,
mocker,
mock_session,
mock_user):
def unauthorized():
raise HTTPException(status_code=401)
user_body = user_factory.user_body_factory(name='test user delete')
app.dependency_overrides[get_current_user] = unauthorized
mock = mocker.patch('src.users.service.delete_one')
response = client.delete('/api/users/2')
assert response.status_code == 401
mock.assert_not_called()
app.dependency_overrides.clear()

View File

@@ -0,0 +1,158 @@
import pytest
import src.forms.exceptions as forms_exceptions
import src.forms.service as forms_service
import tests.factories.forms as forms_factory
from sqlmodel import Session
from src import models
class TestFormsService:
def test_get_all_forms(self, session: Session,
forms: list[models.FormPublic]):
result = forms_service.get_all(session, [], [], False)
assert len(result) == 2
assert result == forms
def test_get_all_forms_filter_productors(
self, session: Session, forms: list[models.FormPublic]):
result = forms_service.get_all(session, [], ['test productor'], False)
assert len(result) == 2
assert result == forms
def test_get_all_forms_filter_season(
self, session: Session, forms: list[models.FormPublic]):
result = forms_service.get_all(session, ['test season 1'], [], False)
assert len(result) == 1
def test_get_all_forms_all_filters(
self, session: Session, forms: list[models.FormPublic]):
result = forms_service.get_all(
session, ['test season 1'], ['test productor'], True)
assert result == forms
def test_get_one_form(self, session: Session,
forms: list[models.FormPublic]):
result = forms_service.get_one(session, forms[0].id)
assert result == forms[0]
def test_get_one_form_notfound(self, session: Session):
result = forms_service.get_one(session, 122)
assert result is None
def test_create_form(
self,
session: Session,
productor: models.ProductorPublic,
referer: models.ProductorPublic
):
form_create = forms_factory.form_create_factory(
name="new test form",
productor_id=productor.id,
referer=referer.id,
season="new test season",
)
result = forms_service.create_one(session, form_create)
assert result.id is not None
assert result.name == "new test form"
assert result.productor.name == "test productor"
def test_create_form_invalidinput(
self,
session: Session,
productor: models.Productor
):
form_create = None
with pytest.raises(forms_exceptions.FormCreateError):
result = forms_service.create_one(session, form_create)
form_create = forms_factory.form_create_factory(productor_id=123)
with pytest.raises(forms_exceptions.ProductorNotFoundError):
result = forms_service.create_one(session, form_create)
form_create = forms_factory.form_create_factory(
productor_id=productor.id,
referer_id=123
)
with pytest.raises(forms_exceptions.UserNotFoundError):
result = forms_service.create_one(session, form_create)
def test_update_form(
self,
session: Session,
productor: models.ProductorPublic,
referer: models.ProductorPublic,
forms: list[models.FormPublic]
):
form_update = forms_factory.form_update_factory(
name='updated test form',
productor_id=productor.id,
referer_id=referer.id,
season='updated test season'
)
form_id = forms[0].id
result = forms_service.update_one(session, form_id, form_update)
assert result.id == form_id
assert result.name == 'updated test form'
assert result.season == 'updated test season'
def test_update_form_notfound(
self,
session: Session,
productor: models.ProductorPublic,
referer: models.ProductorPublic,
):
form_update = forms_factory.form_update_factory(
name='updated test form',
productor_id=productor.id,
referer_id=referer.id,
season='updated test season'
)
form_id = 123
with pytest.raises(forms_exceptions.FormNotFoundError):
result = forms_service.update_one(session, form_id, form_update)
def test_update_form_invalidinput(
self,
session: Session,
productor: models.ProductorPublic,
forms: list[models.FormPublic]
):
form_id = forms[0].id
form_update = forms_factory.form_update_factory(productor_id=123)
with pytest.raises(forms_exceptions.ProductorNotFoundError):
result = forms_service.update_one(session, form_id, form_update)
form_update = forms_factory.form_update_factory(
productor_id=productor.id,
referer_id=123
)
with pytest.raises(forms_exceptions.UserNotFoundError):
result = forms_service.update_one(session, form_id, form_update)
def test_delete_form(
self,
session: Session,
forms: list[models.FormPublic]
):
form_id = forms[0].id
result = forms_service.delete_one(session, form_id)
check = forms_service.get_one(session, form_id)
assert check is None
def test_delete_form_notfound(
self,
session: Session,
forms: list[models.FormPublic]
):
form_id = 123
with pytest.raises(forms_exceptions.FormNotFoundError):
result = forms_service.delete_one(session, form_id)

View File

@@ -0,0 +1,146 @@
import pytest
import src.productors.exceptions as productors_exceptions
import src.productors.service as productors_service
import tests.factories.productors as productors_factory
from sqlmodel import Session
from src import models
class TestProductorsService:
def test_get_all_productors(
self,
session: Session,
productors: list[models.ProductorPublic],
user: models.UserPublic
):
result = productors_service.get_all(session, user, [], [])
assert len(result) == 1
assert result == [productors[0]]
def test_get_all_productors_filter_names(
self,
session: Session,
productors: list[models.ProductorPublic],
user: models.UserPublic
):
result = productors_service.get_all(
session,
user,
['test productor 1'],
[]
)
assert len(result) == 1
def test_get_all_productors_filter_types(
self,
session: Session,
productors: list[models.ProductorPublic],
user: models.UserPublic
):
result = productors_service.get_all(
session,
user,
[],
['Légumineuses'],
)
assert len(result) == 1
def test_get_all_productors_all_filters(
self,
session: Session,
productors: list[models.ProductorPublic],
user: models.UserPublic
):
result = productors_service.get_all(
session,
user,
['test productor 1'],
['Légumineuses'],
)
assert len(result) == 1
def test_get_one_productor(self,
session: Session,
productors: list[models.ProductorPublic]):
result = productors_service.get_one(session, productors[0].id)
assert result == productors[0]
def test_get_one_productor_notfound(self, session: Session):
result = productors_service.get_one(session, 122)
assert result is None
def test_create_productor(
self,
session: Session,
referer: models.ProductorPublic
):
productor_create = productors_factory.productor_create_factory(
name="new test productor",
)
result = productors_service.create_one(session, productor_create)
assert result.id is not None
assert result.name == "new test productor"
def test_create_productor_invalidinput(
self,
session: Session,
):
productor_create = None
with pytest.raises(productors_exceptions.ProductorCreateError):
result = productors_service.create_one(session, productor_create)
def test_update_productor(
self,
session: Session,
referer: models.ProductorPublic,
productors: list[models.ProductorPublic]
):
productor_update = productors_factory.productor_update_factory(
name='updated test productor',
)
productor_id = productors[0].id
result = productors_service.update_one(
session, productor_id, productor_update)
assert result.id == productor_id
assert result.name == 'updated test productor'
def test_update_productor_notfound(
self,
session: Session,
referer: models.ProductorPublic,
):
productor_update = productors_factory.productor_update_factory(
name='updated test productor',
)
productor_id = 123
with pytest.raises(productors_exceptions.ProductorNotFoundError):
result = productors_service.update_one(
session, productor_id, productor_update)
def test_delete_productor(
self,
session: Session,
productors: list[models.ProductorPublic]
):
productor_id = productors[0].id
result = productors_service.delete_one(session, productor_id)
check = productors_service.get_one(session, productor_id)
assert check is None
def test_delete_productor_notfound(
self,
session: Session,
productors: list[models.ProductorPublic]
):
productor_id = 123
with pytest.raises(productors_exceptions.ProductorNotFoundError):
result = productors_service.delete_one(session, productor_id)

View File

@@ -0,0 +1,196 @@
import pytest
import src.products.exceptions as products_exceptions
import src.products.service as products_service
import tests.factories.products as products_factory
from sqlmodel import Session
from src import models
class TestProductsService:
def test_get_all_products(
self,
session: Session,
products: list[models.ProductPublic],
user: models.UserPublic
):
result = products_service.get_all(session, user, [], [], [])
assert len(result) == 2
assert result == products
def test_get_all_products_filter_productors(
self,
session: Session,
products: list[models.ProductPublic],
user: models.UserPublic
):
result = products_service.get_all(
session,
user,
[],
['test productor'],
[]
)
assert len(result) == 2
assert result == products
def test_get_all_products_filter_names(
self,
session: Session,
products: list[models.ProductPublic],
user: models.UserPublic
):
result = products_service.get_all(
session,
user,
['product 1 occasionnal'],
[],
[]
)
assert len(result) == 1
def test_get_all_products_filter_types(
self,
session: Session,
products: list[models.ProductPublic],
user: models.UserPublic
):
result = products_service.get_all(
session,
user,
[],
[],
['1']
)
assert len(result) == 1
def test_get_all_products_all_filters(
self,
session: Session,
products: list[models.ProductPublic],
user: models.UserPublic
):
result = products_service.get_all(
session,
user,
['product 1 occasionnal'],
['test productor'],
['1']
)
assert len(result) == 1
def test_get_one_product(self, session: Session,
products: list[models.ProductPublic]):
result = products_service.get_one(session, products[0].id)
assert result == products[0]
def test_get_one_product_notfound(self, session: Session):
result = products_service.get_one(session, 122)
assert result is None
def test_create_product(
self,
session: Session,
productor: models.ProductorPublic,
referer: models.ProductorPublic
):
product_create = products_factory.product_create_factory(
name="new test product",
productor_id=productor.id,
)
result = products_service.create_one(session, product_create)
assert result.id is not None
assert result.name == "new test product"
assert result.productor.name == "test productor"
def test_create_product_invalidinput(
self,
session: Session,
productor: models.Productor
):
product_create = None
with pytest.raises(products_exceptions.ProductCreateError):
result = products_service.create_one(session, product_create)
product_create = products_factory.product_create_factory(
productor_id=123)
with pytest.raises(products_exceptions.ProductorNotFoundError):
result = products_service.create_one(session, product_create)
def test_update_product(
self,
session: Session,
productor: models.ProductorPublic,
referer: models.ProductorPublic,
products: list[models.ProductPublic]
):
product_update = products_factory.product_update_factory(
name='updated test product',
productor_id=productor.id,
)
product_id = products[0].id
result = products_service.update_one(
session, product_id, product_update)
assert result.id == product_id
assert result.name == 'updated test product'
def test_update_product_notfound(
self,
session: Session,
productor: models.ProductorPublic,
referer: models.ProductorPublic,
):
product_update = products_factory.product_update_factory(
name='updated test product',
productor_id=productor.id,
)
product_id = 123
with pytest.raises(products_exceptions.ProductNotFoundError):
result = products_service.update_one(
session, product_id, product_update)
def test_update_product_invalidinput(
self,
session: Session,
productor: models.ProductorPublic,
products: list[models.ProductPublic]
):
product_id = products[0].id
product_update = products_factory.product_update_factory(
productor_id=123)
with pytest.raises(products_exceptions.ProductorNotFoundError):
result = products_service.update_one(
session, product_id, product_update)
product_update = products_factory.product_update_factory(
productor_id=productor.id,
referer_id=123
)
def test_delete_product(
self,
session: Session,
products: list[models.ProductPublic]
):
product_id = products[0].id
result = products_service.delete_one(session, product_id)
check = products_service.get_one(session, product_id)
assert check is None
def test_delete_product_notfound(
self,
session: Session,
products: list[models.ProductPublic]
):
product_id = 123
with pytest.raises(products_exceptions.ProductNotFoundError):
result = products_service.delete_one(session, product_id)

View File

@@ -0,0 +1,150 @@
import datetime
import pytest
import src.shipments.exceptions as shipments_exceptions
import src.shipments.service as shipments_service
import tests.factories.shipments as shipments_factory
from sqlmodel import Session
from src import models
class TestShipmentsService:
def test_get_all_shipments(
self,
session: Session,
shipments: list[models.ShipmentPublic],
user: models.UserPublic,
):
result = shipments_service.get_all(session, user, [], [], [])
assert len(result) == 2
assert result == shipments
def test_get_all_shipments_filter_names(
self,
session: Session,
shipments: list[models.ShipmentPublic],
user: models.UserPublic,
):
result = shipments_service.get_all(
session, user, ['test shipment 1'], [], [])
assert len(result) == 1
assert result == [shipments[0]]
def test_get_all_shipments_filter_dates(
self,
session: Session,
shipments: list[models.ShipmentPublic],
user: models.UserPublic,
):
result = shipments_service.get_all(
session, user, [], ['2025-10-10'], [])
assert len(result) == 1
def test_get_all_shipments_filter_forms(
self,
session: Session,
shipments: list[models.ShipmentPublic],
forms: list[models.FormPublic],
user: models.UserPublic,
):
result = shipments_service.get_all(
session, user, [], [], [forms[0].name])
assert len(result) == 2
def test_get_all_shipments_all_filters(
self,
session: Session,
shipments: list[models.ShipmentPublic],
forms: list[models.FormPublic],
user: models.UserPublic,
):
result = shipments_service.get_all(session, user, ['test shipment 1'], [
'2025-10-10'], [forms[0].name])
assert len(result) == 1
def test_get_one_shipment(self, session: Session,
shipments: list[models.ShipmentPublic]):
result = shipments_service.get_one(session, shipments[0].id)
assert result == shipments[0]
def test_get_one_shipment_notfound(self, session: Session):
result = shipments_service.get_one(session, 122)
assert result is None
def test_create_shipment(
self,
session: Session,
):
shipment_create = shipments_factory.shipment_create_factory(
name='new test shipment',
date='2025-10-10',
)
result = shipments_service.create_one(session, shipment_create)
assert result.id is not None
assert result.name == "new test shipment"
def test_create_shipment_invalidinput(
self,
session: Session,
):
shipment_create = None
with pytest.raises(shipments_exceptions.ShipmentCreateError):
result = shipments_service.create_one(session, shipment_create)
def test_update_shipment(
self,
session: Session,
shipments: list[models.ShipmentPublic]
):
shipment_update = shipments_factory.shipment_update_factory(
name='updated shipment 1',
date='2025-12-10',
)
shipment_id = shipments[0].id
result = shipments_service.update_one(
session, shipment_id, shipment_update)
assert result.id == shipment_id
assert result.name == 'updated shipment 1'
assert result.date == datetime.date(2025, 12, 10)
def test_update_shipment_notfound(
self,
session: Session,
):
shipment_update = shipments_factory.shipment_update_factory(
name='updated shipment 1',
date=datetime.date(2025, 10, 10),
)
shipment_id = 123
with pytest.raises(shipments_exceptions.ShipmentNotFoundError):
result = shipments_service.update_one(
session, shipment_id, shipment_update)
def test_delete_shipment(
self,
session: Session,
shipments: list[models.ShipmentPublic]
):
shipment_id = shipments[0].id
result = shipments_service.delete_one(session, shipment_id)
check = shipments_service.get_one(session, shipment_id)
assert check is None
def test_delete_shipment_notfound(
self,
session: Session,
shipments: list[models.ShipmentPublic]
):
shipment_id = 123
with pytest.raises(shipments_exceptions.ShipmentNotFoundError):
result = shipments_service.delete_one(session, shipment_id)

View File

@@ -0,0 +1,120 @@
import pytest
import src.users.exceptions as users_exceptions
import src.users.service as users_service
import tests.factories.users as users_factory
from sqlmodel import Session
from src import models
class TestUsersService:
def test_get_all_users(self, session: Session,
users: list[models.UserPublic]):
result = users_service.get_all(session, [], [])
assert len(result) == 3
assert result == users
def test_get_all_users_filter_names(
self, session: Session, users: list[models.UserPublic]):
result = users_service.get_all(session, ['test user 1 (admin)'], [])
assert len(result) == 1
assert result == [users[0]]
def test_get_all_users_filter_emails(
self, session: Session, users: list[models.UserPublic]):
result = users_service.get_all(session, [], ['test1@test.com'])
assert len(result) == 1
def test_get_all_users_all_filters(
self, session: Session, users: list[models.UserPublic]):
result = users_service.get_all(
session, ['test user 1 (admin)'], ['test1@test.com'])
assert len(result) == 1
def test_get_one_user(self, session: Session,
users: list[models.UserPublic]):
result = users_service.get_one(session, users[0].id)
assert result == users[0]
def test_get_one_user_notfound(self, session: Session):
result = users_service.get_one(session, 122)
assert result is None
def test_create_user(
self,
session: Session,
):
user_create = users_factory.user_create_factory(
name="new test user",
email='test@test.fr',
role_names=['test role']
)
result = users_service.create_one(session, user_create)
assert result.id is not None
assert result.name == "new test user"
assert result.email == "test@test.fr"
assert len(result.roles) == 1
def test_create_user_invalidinput(
self,
session: Session,
):
user_create = None
with pytest.raises(users_exceptions.UserCreateError):
result = users_service.create_one(session, user_create)
def test_update_user(
self,
session: Session,
users: list[models.UserPublic]
):
user_update = users_factory.user_update_factory(
name="updated test user",
email='test@testttt.fr',
role_names=['test role']
)
user_id = users[0].id
result = users_service.update_one(session, user_id, user_update)
assert result.id == user_id
assert result.name == 'updated test user'
assert result.email == 'test@testttt.fr'
def test_update_user_notfound(
self,
session: Session,
):
user_update = users_factory.user_update_factory(
name="updated test user",
email='test@testttt.fr',
role_names=['test role']
)
user_id = 123
with pytest.raises(users_exceptions.UserNotFoundError):
result = users_service.update_one(session, user_id, user_update)
def test_delete_user(
self,
session: Session,
users: list[models.UserPublic]
):
user_id = users[0].id
result = users_service.delete_one(session, user_id)
check = users_service.get_one(session, user_id)
assert check is None
def test_delete_user_notfound(
self,
session: Session,
users: list[models.UserPublic]
):
user_id = 123
with pytest.raises(users_exceptions.UserNotFoundError):
result = users_service.delete_one(session, user_id)

View File

@@ -1,6 +1,6 @@
{
"version": "1",
"name": "amapcontract",
"name": "bruno",
"type": "collection",
"ignore": [
"node_modules",

56
docker-compose.dev.yaml Normal file
View File

@@ -0,0 +1,56 @@
services:
frontend:
build:
context: .
dockerfile: frontend/Dockerfile.dev
volumes:
- ./frontend:/app
- /app/node_modules
environment:
VITE_API_URL: ${VITE_API_URL}
ports:
- "5173:5173"
depends_on:
- backend
backend:
build:
context: .
dockerfile: backend/Dockerfile
volumes:
- ./backend:/code
command: >
sh -c "fastapi run src/main.py --reload --port 8000"
environment:
ORIGINS: ${ORIGINS}
DB_HOST: database
DB_USER: ${DB_USER}
DB_PASS: ${DB_PASS}
DB_NAME: ${DB_NAME}
SECRET_KEY: ${SECRET_KEY}
VITE_API_URL: ${VITE_API_URL}
KEYCLOAK_SERVER: ${KEYCLOAK_SERVER}
KEYCLOAK_REALM: ${KEYCLOAK_REALM}
KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID}
KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET}
KEYCLOAK_REDIRECT_URI: ${KEYCLOAK_REDIRECT_URI}
DEBUG: true
MAX_AGE: ${MAX_AGE}
ports:
- "8000:8000"
depends_on:
- database
database:
image: postgres
restart: always
shm_size: 128mb
volumes:
- db:/var/lib/postgresql
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_DB: ${DB_NAME}
ports:
- 5432:5432
volumes:
db:

View File

@@ -1,32 +1,39 @@
version: "3.9"
services:
# nginx:
# restart: always
# build:
# context: .
# dockerfile: front/Dockerfile
# args:
# VUE_APP_ROOT_FQDN: ${SERVICE_ROOT_FQDN}
# ports:
# - 80:80
# depends_on:
# - back
# back:
# build:
# context: .
# dockerfile: back/Dockerfile
# restart: always
# environment:
# SERVICE_ORIGIN: ${SERVICE_ORIGIN}
# DB_HOST: database
# MARIADB_USER: ${MARIADB_USER}
# MARIADB_PASSWORD: ${MARIADB_PASSWORD}
# MARIADB_DATABASE: ${MARIADB_DATABASE}
# SERVICE_SECRET_KEY: ${SERVICE_SECRET_KEY}
# ports:
# - 8000:8000
# depends_on:
# - database
nginx:
restart: always
build:
context: .
dockerfile: frontend/Dockerfile
args:
VITE_API_URL: ${VITE_API_URL}
ports:
- 80:80
depends_on:
- backend
backend:
build:
context: .
dockerfile: backend/Dockerfile
restart: always
environment:
ORIGINS: ${ORIGINS}
DB_HOST: database
DB_USER: ${DB_USER}
DB_PASS: ${DB_PASS}
DB_NAME: ${DB_NAME}
SECRET_KEY: ${SECRET_KEY}
VITE_API_URL: ${VITE_API_URL}
KEYCLOAK_SERVER: ${KEYCLOAK_SERVER}
KEYCLOAK_REALM: ${KEYCLOAK_REALM}
KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID}
KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET}
KEYCLOAK_REDIRECT_URI: ${KEYCLOAK_REDIRECT_URI}
DEBUG: ${DEBUG}
MAX_AGE: ${MAX_AGE}
ports:
- 8000:8000
depends_on:
- database
database:
image: postgres
restart: always
@@ -35,8 +42,7 @@ services:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_DB: ${DB_NAME}
ROOT_FQDN: ${ROOT_FQDN}
ports:
- "54321:5432"
- 5432:5432
volumes:
db:

20
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM node:20.19-alpine AS build
WORKDIR /app
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL
COPY frontend/package.json frontend/package-lock.json /app/
RUN npm install
COPY frontend/ .
RUN npm run build
FROM nginx:latest
COPY --from=build /app/dist /srv/www/frontend
RUN rm /etc/nginx/conf.d/default.conf
COPY --from=build /app/nginx/default.conf /etc/nginx/conf.d/default.conf

12
frontend/Dockerfile.dev Normal file
View File

@@ -0,0 +1,12 @@
FROM node:20 AS dev
WORKDIR /app
COPY frontend/package.json ./
COPY frontend/package-lock.json ./
RUN npm install
COPY frontend .
CMD ["npm", "run", "dev", "--", "--host"]

View File

@@ -75,12 +75,17 @@
"some contracts require a minimum value per shipment, ignore this field if it's not the case": "some contracts require a minimum value per shipment. Ignore this field if it does not apply to your contract.",
"export contracts": "export contracts",
"download recap": "download recap",
"fill contract online": "fill contract online",
"download base template to print": "download base template to print",
"to export contracts submissions before sending to the productor go to the contracts section": "to export contracts submissions before sending to the productor go to the contracts section.",
"in this page you can view all contracts submissions, you can remove duplicates submission or download a specific contract": "in this page you can view all contracts submissions, you can remove duplicates submission or download a specific contract",
"you can download all contracts for your form using the export all": "you can download all contracts for your form using the export all",
"in the same corner you can download a recap by clicking on the button": "in the same corner you can download a recap by clicking on the",
"once all contracts downloaded, you can delete the form (to avoid new submissions) and hide it from the home page": "once all contracts downloaded, you can delete the form (to avoid new submissions) and hide it from the home page",
"by checking this option the form will be accessible publicly on the home page, only check it if everything is fine with your form": "by checking this option the form will be accessible publicly on the home page, only check it if everything is fine with your form",
"contracts": "contracts",
"hidden": "hidden",
"visible": "visible",
"minimum price for this shipment should be at least": "minimum price for this shipment should be at least",
"there is": "there is",
"for this contract": "for this contract.",
@@ -90,6 +95,8 @@
"templates": "templates",
"users": "users",
"forms": "contract forms",
"max cheque number": "max cheque number",
"can be empty default to 3": "can be empty default to 3",
"form": "contract form",
"select a form": "select a form",
"download contracts": "download contracts",
@@ -199,6 +206,7 @@
"your session has expired please log in again": "your session has expired please log in again",
"session expired": "session expired",
"user not allowed": "user not allowed",
"roles": "roles",
"your keycloak user has no roles, please contact your administrator": "your keycloak user has no roles, please contact your administrator",
"choose payment method": "choose your payment method (you do not need to pay now).",
"the product unit will be assigned to the quantity requested in the form": "the product unit defines the unit used in the contract form.",

View File

@@ -73,7 +73,10 @@
"shipment products is necessary only for occasional products (if all products are recurrent leave empty)": "il est nécessaire de configurer les produits pour la livraison uniquement si il y a des produits occasionnels (laisser vide si tous les produits sont récurents).",
"recurrent product is for all shipments, occasional product is for a specific shipment (see shipment form)": "les produits récurrents sont pour toutes les livraisons, les produits occasionnels sont pour une livraison particulière (voir formulaire de création de livraison).",
"some contracts require a minimum value per shipment, ignore this field if it's not the case": "certains contrats nécessitent une valeur minimum par livraison. Ce champ peut être ignoré sil ne sapplique pas à votre contrat.",
"by checking this option the form will be accessible publicly on the home page, only check it if everything is fine with your form": "en cochant cette option le formulaire sera accessible publiquement sur la page d'accueil, cochez cette option uniquement si tout est prêt avec votre formulaire.",
"contracts": "contrats",
"hidden": "caché",
"visible": "visible",
"minimum price for this shipment should be at least": "le prix minimum d'une livraison doit être au moins de",
"there is": "il y a",
"for this contract": "pour ce contrat.",
@@ -89,9 +92,13 @@
"all contracts": "tous les contrats",
"remove contract": "supprimer le contrat",
"download contract": "télécharger le contrat",
"fill contract online": "remplir le contrat en ligne",
"download base template to print": "télécharger le contrat à remplir sur papier",
"by selecting a form here you can download all contracts of your form": "en selectionnant un formulaire, vous téléchargez tous les contrats pour un formulaire donné.",
"edit user": "modifier l'utilisateur·trice",
"remove user": "supprimer l'utilisateur·trice",
"max cheque number": "numbre maximum de cheques possible",
"can be empty default to 3": "optionnel, la valeur par défaut est à 3 cheques",
"all forms": "tous les formulaires de contrat",
"create new form": "créer un nouveau formulaire de contrat",
"actions": "actions",
@@ -162,7 +169,7 @@
"with cheque and transfer": "avec chèques et virements configuré pour le producteur",
"mililiter": "mililitres (ml)",
"this field is optionnal a product can have a quantity if configured inside the product it will be shown inside the form": "ce champ est optionnel dans la configuration d'un produit, il représente la quantité d'un produit (poids d'une tranche de foie, poids d'un panier, taille d'un bocal...). Si ce champs est renseigné il sera affiché dans le formulaire à destination des amapiens.",
"this field is also optionnal if a product have a quantity you can select the correct unit (metric system). It will be shown next to product quantity inside the form": "ce champs est optionnel dans la configuation d'un produit, il représente l'unité de mesure associé à la quantité d'un produit (g, kg, ml, L). Si ce champs est renseigné il sera affiché dans le formulaire à destination des amapiens à coté de la quantité du produit.",
"this field is also optionnal if a product have a quantity you can select the correct unit (metric system). It will be shown next to product quantity inside the form": "ce champs est optionnel dans la configuation d'un produit, il représente l'unité de mesure associée à la quantité d'un produit (g, kg, ml, L). Si ce champs est renseigné il sera affiché dans le formulaire à destination des amapiens à coté de la quantité du produit.",
"with 150 set as quantity and g as quantity unit in product": "avec \"150\" en quantité de produit et \"grammes\" selectionné dans l'unité de quantité du produit",
"all shipments should be recreated for each form creation": "les livraisons étant liées à un formulaire elles doivent être recréés pour chaque nouveau formulaire.",
"a productor can be edited if its informations change, it should not be recreated for each contracts": "un(e) producteur·trice peut être édité si ses informations changent, il/elle ne doit pas être recréé pour chaque nouveau contrat.",
@@ -199,6 +206,7 @@
"your session has expired please log in again": "votre session a expiré veuillez vous reconnecter.",
"session expired": "session expirée",
"user not allowed": "utilisateur non authorisé",
"roles": "roles",
"your keycloak user has no roles, please contact your administrator": "votre utilisateur keycloak n'a pas de roles configurés, contactez votre administrateur.",
"choose payment method": "choisissez votre méthode de paiement (vous n'avez pas à payer tout de suite, uniquement renseigner comment vous souhaitez régler votre commande).",
"the product unit will be assigned to the quantity requested in the form": "l'unité de vente du produit définit l'unité associée à la quantité demandée dans le formulaire des amapiens.",

View File

@@ -0,0 +1,22 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name localhost;
root /srv/www/frontend;
index index.html;
location /api/ {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
proxy_pass http://backend:8000;
}
location / {
try_files $uri /index.html;
}
}

Some files were not shown because too many files have changed in this diff Show More