diff --git a/.github/workflows/build-filebeat-rootless.yml b/.github/workflows/build-filebeat-rootless.yml index 414ee2cef..579407680 100644 --- a/.github/workflows/build-filebeat-rootless.yml +++ b/.github/workflows/build-filebeat-rootless.yml @@ -22,4 +22,4 @@ jobs: with: file: ./infra/filebeat.Dockerfile push: true - tags: ghcr.io/datalab-mi/basegun/filebeat-rootless:6.5.4 + tags: ghcr.io/dnum-mi/basegun/filebeat-rootless:6.5.4 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index f73f8b7ff..000000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,98 +0,0 @@ -on: - workflow_call: - inputs: - image_version: - required: false - type: string - description: "Docker image version to pull for deployment" - default: "latest" - branch: - required: false - description: "Branch to pull in instance deployment" - type: string - default: "main" - volume_size: - required: false - description: "Size in GB of instance" - type: number - default: 10 - flavor: - required: false - type: string - description: "Flavor of instance" - default: "s1-2" - workspace: - required: true - type: string - description: "Workspace used for deployment: prod or preprod" - default: "prod" - secrets: - API_OVH_TOKEN: - required: true - SERVER_IP: - required: true - OS_PASSWORD: - required: true - OS_PROJECT_ID: - required: true - OS_PROJECT_NAME: - required: true - OS_USERNAME: - required: true - X_OVH_TOKEN: - required: true -jobs: - deployment: - name: Deployment - runs-on: ubuntu-20.04 - defaults: - run: - working-directory: ./infra/terraform - env: - API_OVH_TOKEN: ${{ secrets.API_OVH_TOKEN }} - APP_BRANCH: ${{ inputs.branch }} - APP_VERSION: ${{ inputs.image_version }} - OS_AUTH_URL: https://auth.cloud.ovh.net/v3 - OS_IDENTITY_API_VERSION: 3 - OS_INTERFACE: public - OS_PASSWORD: ${{ secrets.OS_PASSWORD }} - OS_PROJECT_NAME: ${{ secrets.OS_PROJECT_NAME }} - OS_PROJECT_ID: ${{ secrets.OS_PROJECT_ID }} - OS_REGION_NAME: "GRA7" - OS_USER_DOMAIN_NAME: "Default" - OS_USERNAME: ${{ secrets.OS_USERNAME }} - TF_VAR_fixed_ip: ${{ secrets.SERVER_IP }} - TF_VAR_flavor: ${{ inputs.flavor }} - TF_VAR_volume_size: ${{ inputs.volume_size }} - WORKSPACE: ${{ inputs.workspace }} - X_OVH_TOKEN: ${{ secrets.X_OVH_TOKEN }} - - steps: - - uses: hashicorp/setup-terraform@v1 - with: - terraform_version: 1.1.8 - - - uses: actions/checkout@v2 - - name: Terraform Init - id: init - run: terraform init - - - name: Terraform Validate - id: validate - run: terraform validate -no-color - - - name: Terraform Workspace - run: terraform workspace select ${WORKSPACE} || terraform workspace new ${WORKSPACE} - - - name: Terraform Taint if exist - run: terraform taint -allow-missing openstack_compute_instance_v2.instance - - - name: Terraform apply - run: | - envsubst < env.tfvars > deployenv.tfvars - terraform apply --auto-approve -var-file="deployenv.tfvars" - -#TODO: -#use if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' -#or on: push -#TODO: run script if server is reachable diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index cbe0cc612..d9cd2777f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -34,8 +34,7 @@ jobs: with: context: ./backend push: true - tags: ghcr.io/datalab-mi/basegun/basegun-backend:${{ github.head_ref }} - target: dev + tags: ghcr.io/dnum-mi/basegun/basegun-backend:${{ github.head_ref }} build-frontend: name: Build Frontend @@ -54,17 +53,15 @@ jobs: with: context: ./frontend push: true - tags: ghcr.io/datalab-mi/basegun/basegun-frontend:${{ github.head_ref }} - target: prod + tags: ghcr.io/dnum-mi/basegun/basegun-frontend:${{ github.head_ref }} test-backend: name: Test Backend needs: build-backend runs-on: ubuntu-latest container: - image: ghcr.io/datalab-mi/basegun/basegun-backend:${{ github.head_ref }} + image: ghcr.io/dnum-mi/basegun/basegun-backend:${{ github.head_ref }} env: - WORKSPACE: dev AWS_REGION: gra AWS_DEFAULT_REGION: gra AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} diff --git a/.github/workflows/preprod.yml b/.github/workflows/preprod.yml index de1da6fa3..2d14c7c67 100644 --- a/.github/workflows/preprod.yml +++ b/.github/workflows/preprod.yml @@ -28,7 +28,7 @@ jobs: uses: vlaurin/action-ghcr-prune@main with: token: ${{ secrets.PERSO_ACCESS_TOKEN }} - organization: datalab-mi + organization: dnum-mi container: basegun/basegun-backend dry-run: false prune-untagged: true @@ -36,7 +36,7 @@ jobs: uses: vlaurin/action-ghcr-prune@main with: token: ${{ secrets.PERSO_ACCESS_TOKEN }} - organization: datalab-mi + organization: dnum-mi container: basegun/basegun-frontend dry-run: false prune-untagged: true diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 51521ceb2..a3c2b22b9 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -47,7 +47,7 @@ jobs: uses: vlaurin/action-ghcr-prune@main with: token: ${{ secrets.PERSO_ACCESS_TOKEN }} - organization: datalab-mi + organization: dnum-mi container: basegun/basegun-backend dry-run: false untagged: true @@ -55,7 +55,7 @@ jobs: uses: vlaurin/action-ghcr-prune@main with: token: ${{ secrets.PERSO_ACCESS_TOKEN }} - organization: datalab-mi + organization: dnum-mi container: basegun/basegun-frontend dry-run: false untagged: true diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 76527a1e4..bc04f81a8 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -11,22 +11,12 @@ jobs: - name: Start stack using docker compose run: docker compose up -d - - - name: Setup nodejs (for cypress) - uses: actions/setup-node@v3 - with: - node-version: 18 - check-latest: true - cache: "npm" - cache-dependency-path: "frontend/package-lock.json" - - name: Install npm packages (for cypress) - run: npm ci - working-directory: ./frontend - - - name: Test end to end (cypress) - run: FRONTEND_HOST=localhost FRONTEND_PORT=3000 npm run test:e2e-ci - working-directory: ./frontend + - name: Cypress run + uses: cypress-io/github-action@v6 + with: + working-directory: ./frontend + command: npm run test:e2e-ci - name: Send artifacts uses: actions/upload-artifact@v3 diff --git a/Makefile b/Makefile index 7e1ddb1e7..218f17186 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ SHELL := /bin/bash DOCKER := $(shell type -p docker) -DC := $(shell type -p docker-compose) +DC := ${DOCKER} compose TAG := 3.3 APP_NAME := basegun REG := ghcr.io -ORG := datalab-mi +ORG := dnum-mi export @@ -29,14 +29,10 @@ check-dc-config-%: check-prerequisites ## Check docker-compose syntax ${DC} config -q build: check-dc-config-% - TAG=${TAG} ${DC} build + ${DC} build up: check-dc-config-% -ifeq ("$(WORKSPACE)","preprod") - TAG=${TAG} PORT_PROD=8080 ${DC} up -d -else - TAG=${TAG} ${DC} up -d -endif + ${DC} up -d down: ${DC} down @@ -58,9 +54,9 @@ pull-%: push: push-${TAG} push-%: - docker tag basegun-frontend:${TAG}-prod ghcr.io/datalab-mi/basegun/basegun-frontend:$* - docker tag basegun-backend:${TAG}-prod ghcr.io/datalab-mi/basegun/basegun-backend:$* - docker push ghcr.io/datalab-mi/basegun/basegun-frontend:$* - docker push ghcr.io/datalab-mi/basegun/basegun-backend:$* + docker tag basegun-frontend:${TAG}-prod ghcr.io/dnum-mi/basegun/basegun-frontend:$* + docker tag basegun-backend:${TAG}-prod ghcr.io/dnum-mi/basegun/basegun-backend:$* + docker push ghcr.io/dnum-mi/basegun/basegun-frontend:$* + docker push ghcr.io/dnum-mi/basegun/basegun-backend:$* deploy-prod: pull up-prod diff --git a/README.md b/README.md index 9b3a4ec08..50c69fb59 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Dependancies: * docker * docker-compose -See also [Debugging](https://github.com/datalab-mi/Basegun/blob/develop/backend/README.md#debugging) section for all the env variables needed for the website to work fully operationally. +See also [Debugging](https://github.com/dnum-mi/Basegun/blob/develop/backend/README.md#debugging) section for all the env variables needed for the website to work fully operationally. ### Install ```bash @@ -52,15 +52,15 @@ Try to find error log * In terminal, run `docker logs basegun-backend` * If you cannot access terminal or don't see anything, go to `localhost:5000/logs` or `preprod.basegun.fr/logs` to see latest logs. -=> ErrorPage "missing model": Download model from the url specified in the [backend Dockerfile](https://github.com/datalab-mi/Basegun/blob/develop/backend/Dockerfile). +=> ErrorPage "missing model": Download model from the url specified in the [backend Dockerfile](https://github.com/dnum-mi/Basegun/blob/develop/backend/Dockerfile). ### The website runs the analysis, but no image shows up Use browser html inspector to find the url given in the image src. * If it starts with `https://storage.gra.cloud.ovh.net` then the website tried to upload the input image to OVH but it failed. Have you set properly in your env the variables OS_USERNAME, OS_PASSWORD and OS_PROJECT ? -* If it starts with `https://localhost` then the website tried to store the input image locally. Have you synchronised the mounts for frontend and backend in [docker-compose](https://github.com/datalab-mi/Basegun/blob/develop/backend/docker-compose.yml) ? (uncomment the `/tmp/basegun` lines in the volumes sections) +* If it starts with `https://localhost` then the website tried to store the input image locally. Have you synchronised the mounts for frontend and backend in [docker-compose](https://github.com/dnum-mi/Basegun/blob/develop/backend/docker-compose.yml) ? (uncomment the `/tmp/basegun` lines in the volumes sections) ### Logs are not sent to the endpoint -The variables `X_OVH_TOKEN` and `API_OVH_TOKEN` must en set in your env. See [Infra README](https://github.com/datalab-mi/Basegun/blob/develop/infra/README.md) for more details. +The variables `X_OVH_TOKEN` and `API_OVH_TOKEN` must en set in your env. See [Infra README](https://github.com/dnum-mi/Basegun/blob/develop/infra/README.md) for more details. ## Release an official version of code 1. Update tag in Makefile diff --git a/apple-touch-icon.png b/apple-touch-icon.png deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/Dockerfile b/backend/Dockerfile index cbe18cbba..96b98ced1 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -13,7 +13,7 @@ RUN apt update && apt install -y \ gcc \ && rm -rf /var/lib/apt/lists/* -# install python libraries (except torch) +# install python libraries COPY requirements.txt . ENV PIP_CERT=$CACERT_LOCATION RUN pip --default-timeout=300 install --upgrade pip \ @@ -22,12 +22,4 @@ RUN pip --default-timeout=300 install --upgrade pip \ ARG VERSION ENV SSL_CERT_FILE=$CACERT_LOCATION -COPY src/ src/ -COPY model.pt . - -FROM base as dev -COPY tests/ tests/ - -FROM base as prod -RUN pip install --extra-index-url https://download.pytorch.org/whl/cpu \ - torch==2.1.1+cpu torchvision==0.16.1+cpu && rm -r /root/.cache \ No newline at end of file +COPY . . \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 89afa3cf3..d0ae5e3c4 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -8,10 +8,11 @@ gelf-formatter==0.2.1 pyyaml>=5.4.1 user-agents==2.2.0 boto3==1.28.39 -torch==2.1.1 -torchvision==0.16.1 -ultralytics==8.1.2 autodynatrace==2.0.0 +# ML +ultralytics==8.1.2 +opencv-python==4.9.0.80 +onnxruntime==1.17.1 # Dev pytest==7.4.3 coverage==7.3.2 \ No newline at end of file diff --git a/backend/src/config.py b/backend/src/config.py new file mode 100644 index 000000000..2edcc82d2 --- /dev/null +++ b/backend/src/config.py @@ -0,0 +1,122 @@ +import os +from datetime import datetime + +import boto3 +from gelfformatter import GelfFormatter + +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) + +PATH_LOGS = os.environ.get("PATH_LOGS", "/tmp/logs") + +LOGS_CONFIG = { + "version": 1, + "disable_existing_loggers": False, + "formatters": {"standard": {"()": lambda: GelfFormatter()}}, + "handlers": { + "default": { + "class": "logging.StreamHandler", + "formatter": "standard", + "level": "INFO", + "stream": "ext://sys.stdout", + }, + "file": { + "class": "logging.handlers.TimedRotatingFileHandler", + "when": "midnight", + "utc": True, + "backupCount": 5, + "level": "INFO", + "filename": f"{PATH_LOGS}/log.json", + "formatter": "standard", + }, + }, + "loggers": {"": {"handlers": ["default", "file"], "level": "DEBUG"}}, +} + +HEADERS = [ + {"name": "Cache-Control", "value": "no-store, max-age=0"}, + {"name": "Clear-Site-Data", "value": '"cache","cookies","storage"'}, + { + "name": "Content-Security-Policy", + "value": "default-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content", + }, + {"name": "Cross-Origin-Embedder-Policy", "value": "require-corp"}, + {"name": "Cross-Origin-Opener-Policy", "value": "same-origin"}, + {"name": "Cross-Origin-Resource-Policy", "value": "same-origin"}, + { + "name": "Permissions-Policy", + "value": "accelerometer=(),ambient-light-sensor=(),autoplay=(),battery=(),camera=(),display-capture=(),document-domain=(),encrypted-media=(),fullscreen=(),gamepad=(),geolocation=(),gyroscope=(),layout-animations=(self),legacy-image-formats=(self),magnetometer=(),microphone=(),midi=(),oversized-images=(self),payment=(),picture-in-picture=(),publickey-credentials-get=(),speaker-selection=(),sync-xhr=(self),unoptimized-images=(self),unsized-media=(self),usb=(),screen-wake-lock=(),web-share=(),xr-spatial-tracking=()", + }, + {"name": "Pragma", "value": "no-cache"}, + {"name": "Referrer-Policy", "value": "no-referrer"}, + { + "name": "Strict-Transport-Security", + "value": "max-age=31536000 ; includeSubDomains", + }, + {"name": "X-Content-Type-Options", "value": "nosniff"}, + {"name": "X-Frame-Options", "value": "deny"}, + {"name": "X-Permitted-Cross-Domain-Policies", "value": "none"}, +] + + +def get_device(user_agent) -> str: + """Explicitly gives the device of a user-agent object + + Args: + user_agent: info given by the user browser + + Returns: + str: mobile, pc, tablet or other + """ + if user_agent.is_mobile: + return "mobile" + elif user_agent.is_pc: + return "pc" + elif user_agent.is_tablet: + return "tablet" + else: + return "other" + + +def get_base_logs(user_agent, user_id: str) -> dict: + """Generates the common information for custom logs in basegun. + Each function can add some info specific to the current process, + then we insert these custom logs as extra + + Args: + user_agent: user agent object + user_id (str): UUID identifying a unique user + + Returns: + dict: the base custom information + """ + extras_logging = { + "bg_date": datetime.now().isoformat(), + "bg_user_id": user_id, + "bg_version": APP_VERSION, + "bg_model": MODEL_VERSION, + "bg_device": get_device(user_agent), + "bg_device_family": user_agent.device.family, + "bg_device_os": user_agent.os.family, + "bg_device_browser": user_agent.browser.family, + } + return extras_logging + + +# Object storage +S3_URL_ENDPOINT = os.environ["S3_URL_ENDPOINT"] +S3_BUCKET_NAME = os.environ["S3_BUCKET_NAME"] +S3_PREFIX = os.path.join("uploaded-images/") + +S3 = boto3.resource("s3", endpoint_url=S3_URL_ENDPOINT, verify=False) + +# Versions +APP_VERSION = "-1" +MODEL_VERSION = "-1" + +TYPOLOGIES_MEASURED = [ + "epaule_a_levier_sous_garde", + "epaule_a_pompe", + "epaule_a_un_coup_par_canon", + "epaule_a_verrou", + "epaule_semi_auto_style_chasse", +] \ No newline at end of file diff --git a/backend/src/constants.py b/backend/src/constants.py deleted file mode 100644 index 20cc489e5..000000000 --- a/backend/src/constants.py +++ /dev/null @@ -1,24 +0,0 @@ -HEADERS = [ - {"name": "Cache-Control", "value": "no-store, max-age=0"}, - {"name": "Clear-Site-Data", "value": '"cache","cookies","storage"'}, - { - "name": "Content-Security-Policy", - "value": "default-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content", - }, - {"name": "Cross-Origin-Embedder-Policy", "value": "require-corp"}, - {"name": "Cross-Origin-Opener-Policy", "value": "same-origin"}, - {"name": "Cross-Origin-Resource-Policy", "value": "same-origin"}, - { - "name": "Permissions-Policy", - "value": "accelerometer=(),ambient-light-sensor=(),autoplay=(),battery=(),camera=(),display-capture=(),document-domain=(),encrypted-media=(),fullscreen=(),gamepad=(),geolocation=(),gyroscope=(),layout-animations=(self),legacy-image-formats=(self),magnetometer=(),microphone=(),midi=(),oversized-images=(self),payment=(),picture-in-picture=(),publickey-credentials-get=(),speaker-selection=(),sync-xhr=(self),unoptimized-images=(self),unsized-media=(self),usb=(),screen-wake-lock=(),web-share=(),xr-spatial-tracking=()", - }, - {"name": "Pragma", "value": "no-cache"}, - {"name": "Referrer-Policy", "value": "no-referrer"}, - { - "name": "Strict-Transport-Security", - "value": "max-age=31536000 ; includeSubDomains", - }, - {"name": "X-Content-Type-Options", "value": "nosniff"}, - {"name": "X-Frame-Options", "value": "deny"}, - {"name": "X-Permitted-Cross-Domain-Policies", "value": "none"}, -] diff --git a/backend/src/main.py b/backend/src/main.py index 2a1f00830..26b9dd272 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -1,132 +1,14 @@ -import json -import sys import logging import os -import time -from datetime import datetime -from contextlib import asynccontextmanager -from typing import Union -from uuid import uuid4 -import boto3 -from fastapi import ( - APIRouter, - BackgroundTasks, - Cookie, - FastAPI, - File, - Form, - HTTPException, - Request, - Response, - UploadFile, -) +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import PlainTextResponse -from gelfformatter import GelfFormatter -from src.constants import HEADERS -from src.model import load_model_inference, predict_image -from user_agents import parse - -CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) - -def setup_logs(log_dir: str) -> logging.Logger: - """Setups environment for logs - - Args: - log_dir (str): folder for log storage - logging.Logger: logger object - """ - formatter = GelfFormatter() - logger = logging.getLogger("Basegun") - logger.setLevel(logging.DEBUG) - handler = logging.StreamHandler(sys.stdout) - handler.setFormatter(formatter) - logger.addHandler(handler) - return logger - - -def get_device(user_agent) -> str: - """Explicitly gives the device of a user-agent object - - Args: - user_agent: info given by the user browser - - Returns: - str: mobile, pc, tablet or other - """ - if user_agent.is_mobile: - return "mobile" - elif user_agent.is_pc: - return "pc" - elif user_agent.is_tablet: - return "tablet" - else: - return "other" - - -def get_base_logs(user_agent, user_id: str) -> dict: - """Generates the common information for custom logs in basegun. - Each function can add some info specific to the current process, - then we insert these custom logs as extra - - Args: - user_agent: user agent object - user_id (str): UUID identifying a unique user - - Returns: - dict: the base custom information - """ - extras_logging = { - "bg_date": datetime.now().isoformat(), - "bg_user_id": user_id, - "bg_version": APP_VERSION, - "bg_model": MODEL_VERSION, - "bg_device": get_device(user_agent), - "bg_device_family": user_agent.device.family, - "bg_device_os": user_agent.os.family, - "bg_device_browser": user_agent.browser.family, - } - return extras_logging - -def upload_image(content: bytes, image_key: str): - """Uploads an image to s3 bucket - path uploaded-images/WORKSPACE/img_name - where WORKSPACE is dev, preprod or prod +from .config import HEADERS, LOGS_CONFIG, PATH_LOGS +from .router import router - Args: - content (bytes): file content - image_key (str): path we want to have - """ - start = time.time() - object = s3.Object(S3_BUCKET_NAME, image_key) - object.put(Body=content) - extras_logging = { - "bg_date": datetime.now().isoformat(), - "bg_upload_time": time.time() - start, - "bg_image_url": image_key, - } - logger.info("Upload successful", extra=extras_logging) - - -#################### -# SETUP # -#################### - -# FastAPI Setup app = FastAPI(docs_url="/api/docs") -router = APIRouter(prefix="/api") -origins = [ # allow requests from front-end - "http://basegun.fr", - "https://basegun.fr", - "http://preprod.basegun.fr", - "https://preprod.basegun.fr", - "http://localhost", - "http://localhost:8080", - "http://localhost:3000", -] app.add_middleware( CORSMiddleware, allow_origins=["*"], @@ -145,164 +27,7 @@ async def add_owasp_middleware(request: Request, call_next): # Logs -PATH_LOGS = os.environ.get("PATH_LOGS", "/tmp/logs") -logger = setup_logs(PATH_LOGS) - -# Load model -app.model = load_model_inference("./model.pt") - -# Object storage -S3_URL_ENDPOINT = os.environ["S3_URL_ENDPOINT"] -S3_BUCKET_NAME = os.environ["S3_BUCKET_NAME"] -S3_PREFIX = os.path.join("uploaded-images/", os.environ["WORKSPACE"]) - -s3 = boto3.resource("s3", endpoint_url=S3_URL_ENDPOINT, verify=False) - -# Versions -if "versions.json" in os.listdir(os.path.dirname(CURRENT_DIR)): - with open("versions.json", "r") as f: - versions = json.load(f) - APP_VERSION = versions["app"] - MODEL_VERSION = versions["model"] -else: - logger.warn("File versions.json not found") - APP_VERSION = "-1" - MODEL_VERSION = "-1" - - -#################### -# ROUTES # -#################### -@router.get("/", response_class=PlainTextResponse) -def home(): - return "Basegun backend" - - -@router.get("/version", response_class=PlainTextResponse) -def version(): - return APP_VERSION - - -@router.post("/upload") -async def imageupload( - request: Request, - response: Response, - background_tasks: BackgroundTasks, - image: UploadFile = File(...), - date: float = Form(...), - user_id: Union[str, None] = Cookie(None), -): - - # prepare content logs - user_agent = parse(request.headers.get("user-agent")) - extras_logging = get_base_logs(user_agent, user_id) - extras_logging["bg_upload_time"] = round(time.time() - date, 2) - - try: - img_key = os.path.join( - S3_PREFIX, str(uuid4()) + os.path.splitext(image.filename)[1].lower() - ) - img_bytes = image.file.read() - - # upload image to OVH Cloud - background_tasks.add_task(upload_image, img_bytes, img_key) - extras_logging["bg_image_url"] = img_key - - # set user id - if not user_id: - user_id = uuid4() - response.set_cookie(key="user_id", value=user_id) - extras_logging["bg_user_id"] = user_id - - # send image to model for prediction - start = time.time() - label, confidence = predict_image(app.model, img_bytes) - extras_logging["bg_label"] = label - extras_logging["bg_confidence"] = confidence - extras_logging["bg_model_time"] = round(time.time() - start, 2) - if confidence < 0.76: - extras_logging["bg_confidence_level"] = "low" - elif confidence < 0.98: - extras_logging["bg_confidence_level"] = "medium" - else: - extras_logging["bg_confidence_level"] = "high" - - logger.info("Identification request", extra=extras_logging) - - return { - "path": img_key, - "label": label, - "confidence": confidence, - "confidence_level": extras_logging["bg_confidence_level"], - } - - except Exception as e: - extras_logging["bg_error_type"] = e.__class__.__name__ - logger.exception(e, extra=extras_logging) - raise HTTPException(status_code=500, detail=str(e)) - - -@router.post("/identification-feedback") -async def log_feedback(request: Request, user_id: Union[str, None] = Cookie(None)): - res = await request.json() - - user_agent = parse(request.headers.get("user-agent")) - extras_logging = get_base_logs(user_agent, user_id) - - extras_logging["bg_feedback_bool"] = res["feedback"] - for key in ["image_url", "label", "confidence", "confidence_level"]: - extras_logging["bg_" + key] = res[key] - - logger.info("Identification feedback", extra=extras_logging) - return - - -@router.post("/tutorial-feedback") -async def log_tutorial_feedback( - request: Request, user_id: Union[str, None] = Cookie(None) -): - res = await request.json() - - user_agent = parse(request.headers.get("user-agent")) - extras_logging = get_base_logs(user_agent, user_id) - - for key in [ - "image_url", - "label", - "confidence", - "confidence_level", - "tutorial_feedback", - "tutorial_option", - "route_name", - ]: - extras_logging["bg_" + key] = res[key] - - logger.info("Tutorial feedback", extra=extras_logging) - return - - -@router.post("/identification-dummy") -async def log_identification_dummy( - request: Request, user_id: Union[str, None] = Cookie(None) -): - res = await request.json() - - user_agent = parse(request.headers.get("user-agent")) - extras_logging = get_base_logs(user_agent, user_id) - - # to know if the firearm is dummy or real - extras_logging["bg_dummy_bool"] = res["is_dummy"] - for key in [ - "image_url", - "label", - "confidence", - "confidence_level", - "tutorial_option", - ]: - extras_logging["bg_" + key] = res[key] - - logger.info("Identification dummy", extra=extras_logging) - return - +os.makedirs(PATH_LOGS, exist_ok=True) +logging.config.dictConfig(LOGS_CONFIG) app.include_router(router) diff --git a/backend/src/ml/measure/best_card.onnx b/backend/src/ml/measure/best_card.onnx new file mode 100644 index 000000000..f33097886 Binary files /dev/null and b/backend/src/ml/measure/best_card.onnx differ diff --git a/backend/src/ml/measure/best_keypoints.pt b/backend/src/ml/measure/best_keypoints.pt new file mode 100644 index 000000000..7552a2921 Binary files /dev/null and b/backend/src/ml/measure/best_keypoints.pt differ diff --git a/backend/src/ml/measure/measure.py b/backend/src/ml/measure/measure.py new file mode 100644 index 000000000..3ae4ba364 --- /dev/null +++ b/backend/src/ml/measure/measure.py @@ -0,0 +1,359 @@ +import cv2 +import numpy as np +import onnxruntime as ort +from ultralytics import YOLO + +NMS_THRES = 0.1 +CONF_THRES = 0 +PI = 3.141592 + +# DOTA-v1.5 +CLASSES = ["Card"] + +# Class YOLOV5_OBB from repository + + +class YOLOv5_OBB: + def __init__(self, model_path, stride=32): + self.model_path = model_path + self.stride = stride + + def rbox2poly(self, obboxes): + """ + Trans rbox format to poly format. + Args: + rboxes (array/tensor): (num_gts, [cx cy l s θ]) θ∈[-pi/2, pi/2) + + Returns: + polys (array/tensor): (num_gts, [x1 y1 x2 y2 x3 y3 x4 y4]) + """ + center, w, h, theta = np.split(obboxes, (2, 3, 4), axis=-1) + Cos, Sin = np.cos(theta), np.sin(theta) + vector1 = np.concatenate([w / 2 * Cos, -w / 2 * Sin], axis=-1) + vector2 = np.concatenate([-h / 2 * Sin, -h / 2 * Cos], axis=-1) + + point1 = center + vector1 + vector2 + point2 = center + vector1 - vector2 + point3 = center - vector1 - vector2 + point4 = center - vector1 + vector2 + order = obboxes.shape[:-1] + return np.concatenate([point1, point2, point3, point4], axis=-1).reshape( + *order, 8 + ) + + def scale_polys(self, img1_shape, polys, img0_shape, ratio_pad=None): + # ratio_pad: [(h_raw, w_raw), (hw_ratios, wh_paddings)] + # Rescale coords (xyxyxyxy) from img1_shape to img0_shape + if ratio_pad is None: # calculate from img0_shape + gain = min( + img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1] + ) # gain = resized / raw + pad = (img1_shape[1] - img0_shape[1] * gain) / 2, ( + img1_shape[0] - img0_shape[0] * gain + ) / 2 # wh padding + else: + gain = ratio_pad[0][0] # h_ratios + pad = ratio_pad[1] # wh_paddings + polys[:, [0, 2, 4, 6]] -= pad[0] # x padding + polys[:, [1, 3, 5, 7]] -= pad[1] # y padding + polys[:, :8] /= gain # Rescale poly shape to img0_shape + # clip_polys(polys, img0_shape) + return polys + + def letterbox( + self, + im, + new_shape, + color=(255, 0, 255), + auto=False, + scaleFill=False, + scaleup=True, + ): + """ + Resize and pad image while meeting stride-multiple constraints + Returns: + im (array): (height, width, 3) + ratio (array): [w_ratio, h_ratio] + (dw, dh) (array): [w_padding h_padding] + """ + shape = im.shape[:2] # current shape [height, width] + if isinstance(new_shape, int): # [h_rect, w_rect] + new_shape = (new_shape, new_shape) + + # Scale ratio (new / old) + r = min(new_shape[0] / shape[0], new_shape[1] / shape[1]) + if not scaleup: # only scale down, do not scale up (for better val mAP) + r = min(r, 1.0) + + # Compute padding + ratio = r, r # wh ratios + new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r)) # w h + dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] # wh padding + + if auto: # minimum rectangle + dw, dh = np.mod(dw, self.stride), np.mod(dh, self.stride) # wh padding + elif scaleFill: # stretch + dw, dh = 0.0, 0.0 + new_unpad = (new_shape[1], new_shape[0]) # [w h] + ratio = ( + new_shape[1] / shape[1], + new_shape[0] / shape[0], + ) # [w_ratio, h_ratio] + + dw /= 2 # divide padding into 2 sides + dh /= 2 + if shape[::-1] != new_unpad: # resize + im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR) + top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) + left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) + im = cv2.copyMakeBorder( + im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color + ) # add border + return im, ratio, (dw, dh) + + def preprocess(self, img, new_shape): + img = self.letterbox(img, new_shape, auto=False)[0] + img = img.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB + img = np.ascontiguousarray(img).astype("float32") + img /= 255 # 0 - 255 to 0.0 - 1.0 + if len(img.shape) == 3: + img = img[None] # expand for batch dim + return img + + def postprecess(self, prediction, src_img, new_shape): + nc = prediction.shape[2] - 5 - 180 # number of classes + maxconf = np.max(prediction[..., 4]) + CONF_THRES = ( + maxconf - 0.1 + ) # To retrieve best results and not limit to an absolute threshold + xc = prediction[..., 4] > CONF_THRES + outputs = prediction[:][xc] + + generate_boxes, bboxes, scores = [], [], [] + + for out in outputs: + cx, cy, longside, shortside, obj_score = out[:5] + class_scores = out[5 : 5 + nc] + class_idx = np.argmax(class_scores) + + max_class_score = class_scores[class_idx] * obj_score + if max_class_score < CONF_THRES: + continue + + theta_scores = out[5 + nc :] + theta_idx = np.argmax(theta_scores) + theta_pred = (theta_idx - 90) / 180 * PI + + bboxes.append([[cx, cy], [longside, shortside], max_class_score]) + scores.append(max_class_score) + generate_boxes.append( + [cx, cy, longside, shortside, theta_pred, max_class_score, class_idx] + ) + + indices = cv2.dnn.NMSBoxesRotated(bboxes, scores, CONF_THRES, NMS_THRES) + det = np.array(generate_boxes)[indices.flatten()] + + pred_poly = self.rbox2poly(det[:, :5]) + + pred_poly = self.scale_polys(new_shape, pred_poly, src_img.shape) + det = np.concatenate((pred_poly, det[:, -2:]), axis=1) # (n, [poly conf cls]) + return det + + def run(self, src_img): + net = ort.InferenceSession(self.model_path, providers=["CPUExecutionProvider"]) + input_name = net.get_inputs()[0].name + input_shape = net.get_inputs()[0].shape + new_shape = input_shape[-2:] + + blob = self.preprocess(src_img, new_shape) + outputs = net.run(None, {input_name: blob})[0] + return self.postprecess(outputs, src_img, new_shape) + + +def get_card(image, model): + """Predict the keypoints on the image + Args: + image (opencv matrix): image after CV2.imread(path) + modelCard (model): model after load_models call + + Returns: + Prediction: Oriented boundng box(x,y,x,y,x,y,x,y ,CONF_THRES, NMS_THRES) + """ + + return model.run(image) + + +def get_keypoints(image, model): + """Predict the keypoints on the image + Args: + image (opencv matrix): image after CV2.imread(path) + modelWeapon (model): model after load_models call + + Returns: + Prediction: keypoints coordinates [[KP1x,KP1y],[KP2x,KP2y],[KP3x,KP3y],[KP4x,KP4y]] + """ + results = model(image, verbose=False) + return results[0].keypoints.data[0] + + +def load_models(model_card_path, model_weapon_path): + """Load model structure and weights + Args: + model_card (str): path to model (.onnx file) + modelWeapon (str): path to model (.pt file) + + Returns: + Models: loaded models ready for prediction and warmed-up + """ + model_card = YOLOv5_OBB(model_path=model_card_path, stride=32) + + model_weapon = YOLO(model_weapon_path) + + # warmup + imagetest = cv2.imread("./src/ml/measure/warmup.jpg") + get_card(imagetest, model_card) + get_keypoints(imagetest, model_weapon) + + return model_card, model_weapon + + +model_card, model_weapon = load_models( + "./src/ml/measure/best_card.onnx", "./src/ml/measure/best_keypoints.pt" +) + + +# geometric functions for distance calculation + + +def distanceCalculate(p1, p2): + """Distance calculation between two points + Args: + P1 (tuple): (x1,y1) + P2 (tuple): (x2,y2) + + Returns: + Distance: float in px + """ + dis = ((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2) ** 0.5 + return dis + + +def scalarproduct(v1, v2): + """Scalar product between two vectors + Args: + P1 (vector): (u1,v1) + P2 (vector): (u2,v2) + + Returns: + Projection: float in px + """ + return (v1[0] * v2[0] + v1[1] * v2[1]) / np.linalg.norm(v2) + + +def rotate(img): + """Rotate the image if not in landscape + Args: + image (opencv matrix): image after CV2.imread(path) + Returns: + image (opencv matrix): image after CV2.imread(path) + """ + height, width, channels = img.shape + if height > width: + img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE) + return img + + +def get_lengths_from_image(img_bytes, draw=True, output_filename="result.jpg"): + """Predict the keypoints on the image + Args: + image (opencv matrix): image after CV2.imread(path) + modelCard (model): model after load_models call + modelWeapon (model): model after load_models call + draw (Boolean): whether the result image need to be drawed and saved + output_filename: Filename and location for the image output + + Returns: + Length (list): Overall Length, Barrel Length, Card detection confidence score + """ + try: + image = np.asarray(bytearray(img_bytes), dtype="uint8") + image = cv2.imdecode(image, cv2.IMREAD_COLOR) + image = rotate(image) + + keypoints = get_keypoints(image, model_weapon) + if keypoints[3][0] < keypoints[0][0]: # Weapon upside down + image = cv2.rotate(image, cv2.ROTATE_180) + keypoints = get_keypoints(image, model_weapon) + + cards = get_card(image, model_card) + card = cards[0] + confCard = card[8] + CardP = distanceCalculate((card[0], card[1]), (card[4], card[5])) + CardP = distanceCalculate((card[2], card[3]), (card[6], card[7])) + CardR = (8.56**2 + 5.398**2) ** 0.5 + + factor = CardR / CardP + canonP = distanceCalculate( + (int(keypoints[2][0]), int(keypoints[2][1])), + (int(keypoints[3][0]), int(keypoints[3][1])), + ) + canonR = round(canonP * factor, 2) + + totalP1 = scalarproduct(keypoints[0] - keypoints[3], keypoints[2] - keypoints[3]) + totalP2 = scalarproduct(keypoints[1] - keypoints[3], keypoints[2] - keypoints[3]) + + totalP = float(max(totalP1, totalP2)) + + totalR = round(totalP * factor, 2) + + if draw: + img2 = image + for keypoint in keypoints: + img2 = cv2.circle( + img2, + (int(keypoint[0]), int(keypoint[1])), + radius=5, + color=(0, 0, 255), + thickness=20, + ) + + img2 = cv2.line( + img2, + (int(card[0]), int(card[1])), + (int(card[2]), int(card[3])), + color=(255, 0, 0), + thickness=15, + ) + img2 = cv2.line( + img2, + (int(card[4]), int(card[5])), + (int(card[2]), int(card[3])), + color=(255, 0, 0), + thickness=15, + ) + img2 = cv2.line( + img2, + (int(card[6]), int(card[7])), + (int(card[4]), int(card[5])), + color=(255, 0, 0), + thickness=15, + ) + img2 = cv2.line( + img2, + (int(card[0]), int(card[1])), + (int(card[6]), int(card[7])), + color=(255, 0, 0), + thickness=15, + ) + img2 = cv2.line( + img2, + (int(card[0]), int(card[1])), + (int(card[4]), int(card[5])), + color=(255, 255, 0), + thickness=15, + ) + + cv2.imwrite(output_filename, img2) + except cv2.error as e: + totalR, canonR, confCard = None, None, None + return (totalR, canonR, confCard) diff --git a/backend/src/ml/measure/warmup.jpg b/backend/src/ml/measure/warmup.jpg new file mode 100644 index 000000000..9457c6ac3 Binary files /dev/null and b/backend/src/ml/measure/warmup.jpg differ diff --git a/backend/model.pt b/backend/src/ml/models/typology.pt similarity index 100% rename from backend/model.pt rename to backend/src/ml/models/typology.pt diff --git a/backend/src/model.py b/backend/src/ml/utils/typology.py similarity index 68% rename from backend/src/model.py rename to backend/src/ml/utils/typology.py index cdc418e9a..b90a214d2 100644 --- a/backend/src/model.py +++ b/backend/src/ml/utils/typology.py @@ -1,7 +1,6 @@ from io import BytesIO from typing import Union -import numpy as np from PIL import Image from ultralytics import YOLO @@ -20,20 +19,10 @@ "semi_auto_style_militaire_autre", ] +MODEL = YOLO("./src/ml/models/typology.pt") -def load_model_inference(model_path: str): - """Load model structure and weights - Args: - model_path (str): path to model (.pt file) - - Returns: - Model: loaded model ready for prediction and Warm-up - """ - return YOLO(model_path) - - -def predict_image(model, img: bytes) -> Union[str, float]: +def get_typology_from_image(img: bytes) -> Union[str, float]: """Run the model prediction on an image Args: @@ -44,7 +33,7 @@ def predict_image(model, img: bytes) -> Union[str, float]: Union[str, float]: (label, confidence) of best class predicted """ im = Image.open(BytesIO(img)) - results = model(im, verbose=False) + results = MODEL(im, verbose=False) predicted_class = results[0].probs.top5[0] label = CLASSES[predicted_class] confidence = float(results[0].probs.top5conf[0]) diff --git a/backend/src/router.py b/backend/src/router.py new file mode 100644 index 000000000..9fe41c82f --- /dev/null +++ b/backend/src/router.py @@ -0,0 +1,156 @@ +import logging +import os +import time +from typing import Union +from uuid import uuid4 + +from fastapi import (APIRouter, BackgroundTasks, Cookie, File, Form, + HTTPException, Request, Response, UploadFile) +from fastapi.responses import PlainTextResponse +from user_agents import parse + +from .config import APP_VERSION, S3_PREFIX, TYPOLOGIES_MEASURED, get_base_logs +from .ml.measure.measure import get_lengths_from_image +from .ml.utils.typology import get_typology_from_image +from .utils import upload_image + +router = APIRouter(prefix="/api") + + +@router.get("/", response_class=PlainTextResponse) +def home(): + return "Basegun backend" + + +@router.get("/version", response_class=PlainTextResponse) +def version(): + return APP_VERSION + + +@router.post("/upload") +async def imageupload( + request: Request, + response: Response, + background_tasks: BackgroundTasks, + image: UploadFile = File(...), + date: float = Form(...), + user_id: Union[str, None] = Cookie(None), +): + # prepare content logs + user_agent = parse(request.headers.get("user-agent")) + extras_logging = get_base_logs(user_agent, user_id) + extras_logging["bg_upload_time"] = round(time.time() - date, 2) + + try: + img_key = os.path.join( + S3_PREFIX, str(uuid4()) + os.path.splitext(image.filename)[1].lower() + ) + img_bytes = image.file.read() + + # upload image to OVH Cloud + background_tasks.add_task(upload_image, img_bytes, img_key) + extras_logging["bg_image_url"] = img_key + + # set user id + if not user_id: + user_id = uuid4() + response.set_cookie(key="user_id", value=user_id) + extras_logging["bg_user_id"] = user_id + + start = time.time() + # Process image with ML models + label, confidence = get_typology_from_image(img_bytes) + + gun_length, gun_barrel_length, conf_card = None, None, None + if label in TYPOLOGIES_MEASURED: + gun_length, gun_barrel_length, conf_card = get_lengths_from_image(img_bytes) + + extras_logging["bg_label"] = label + extras_logging["bg_confidence"] = confidence + extras_logging["bg_gun_length"] = gun_length + extras_logging["bg_gun_barrel_length"] = gun_barrel_length + extras_logging["bg_conf_card"] = conf_card + extras_logging["bg_model_time"] = round(time.time() - start, 2) + if confidence < 0.76: + extras_logging["bg_confidence_level"] = "low" + elif confidence < 0.98: + extras_logging["bg_confidence_level"] = "medium" + else: + extras_logging["bg_confidence_level"] = "high" + + logging.info("Identification request", extra=extras_logging) + + return { + "path": img_key, + "label": label, + "confidence": confidence, + "confidence_level": extras_logging["bg_confidence_level"], + "gun_length": gun_length, + "gun_barrel_length": gun_barrel_length, + "conf_card": conf_card, + } + + except Exception as e: + extras_logging["bg_error_type"] = e.__class__.__name__ + logging.exception(e, extra=extras_logging) + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/identification-feedback") +async def log_feedback(request: Request, user_id: Union[str, None] = Cookie(None)): + res = await request.json() + + user_agent = parse(request.headers.get("user-agent")) + extras_logging = get_base_logs(user_agent, user_id) + + extras_logging["bg_feedback_bool"] = res["feedback"] + for key in ["image_url", "label", "confidence", "confidence_level"]: + extras_logging["bg_" + key] = res[key] + + logging.info("Identification feedback", extra=extras_logging) + + +@router.post("/tutorial-feedback") +async def log_tutorial_feedback( + request: Request, user_id: Union[str, None] = Cookie(None) +): + res = await request.json() + + user_agent = parse(request.headers.get("user-agent")) + extras_logging = get_base_logs(user_agent, user_id) + + for key in [ + "image_url", + "label", + "confidence", + "confidence_level", + "tutorial_feedback", + "tutorial_option", + "route_name", + ]: + extras_logging["bg_" + key] = res[key] + + logging.info("Tutorial feedback", extra=extras_logging) + + +@router.post("/identification-dummy") +async def log_identification_dummy( + request: Request, user_id: Union[str, None] = Cookie(None) +): + res = await request.json() + + user_agent = parse(request.headers.get("user-agent")) + extras_logging = get_base_logs(user_agent, user_id) + + # to know if the firearm is dummy or real + extras_logging["bg_dummy_bool"] = res["is_dummy"] + for key in [ + "image_url", + "label", + "confidence", + "confidence_level", + "tutorial_option", + ]: + extras_logging["bg_" + key] = res[key] + + logging.info("Identification dummy", extra=extras_logging) diff --git a/backend/src/utils.py b/backend/src/utils.py new file mode 100644 index 000000000..8ac2eaa59 --- /dev/null +++ b/backend/src/utils.py @@ -0,0 +1,23 @@ +import logging +import time +from datetime import datetime + +from .config import S3, S3_BUCKET_NAME + + +def upload_image(content: bytes, image_key: str): + """Uploads an image to s3 bucket + path uploaded-images/img_name + Args: + content (bytes): file content + image_key (str): path we want to have + """ + start = time.time() + object = S3.Object(S3_BUCKET_NAME, image_key) + object.put(Body=content) + extras_logging = { + "bg_date": datetime.now().isoformat(), + "bg_upload_time": time.time() - start, + "bg_image_url": image_key, + } + logging.info("Upload successful", extra=extras_logging) diff --git a/backend/tests/epaule_a_levier_sous_garde.jpg b/backend/tests/epaule_a_levier_sous_garde.jpg new file mode 100644 index 000000000..5e41c4a7a Binary files /dev/null and b/backend/tests/epaule_a_levier_sous_garde.jpg differ diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 750dda0ab..31430a794 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -7,7 +7,8 @@ import pytest import requests from fastapi.testclient import TestClient -from src.main import S3_BUCKET_NAME, S3_URL_ENDPOINT, app +from src.config import S3_BUCKET_NAME, S3_URL_ENDPOINT +from src.main import app client = TestClient(app) @@ -65,10 +66,8 @@ def check_log_base(self, log): def test_upload(self): """Checks that the file upload works properly""" - if os.environ["WORKSPACE"] == "dev": - create_bucket() + create_bucket() path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "revolver.jpg") - geoloc = "12.666,7.666" with open(path, "rb") as f: r = client.post( @@ -83,6 +82,8 @@ def test_upload(self): assert res["label"] == "revolver" assert res["confidence"] == pytest.approx(1, 0.1) assert res["confidence_level"] == "high" + assert "gun_length" in res + assert "gun_barrel_length" in res def test_feedback_and_logs(self): """Checks that the feedback works properly""" @@ -117,3 +118,36 @@ def test_headers(self): for header_to_add in HEADERS_TO_ADD: assert header_to_add["name"].lower() in CURRENT_HEADERS + +class TestUpload: + def test_revolver_without_card(self): + with open("./tests/revolver.jpg", "rb") as f: + response = client.post( + "/api/upload", + files={"image": f}, + data={"date": time.time()}, + ) + response.data = response.json() + assert response.status_code == 200 + assert response.data["label"] == "revolver" + assert response.data["confidence"] == pytest.approx(1, 0.1) + assert response.data["confidence_level"] == "high" + assert response.data["gun_length"] is None + assert response.data["gun_barrel_length"] is None + assert response.data["conf_card"] is None + + def test_semi_auto_without_card(self): + with open("./tests/epaule_a_levier_sous_garde.jpg", "rb") as f: + response = client.post( + "/api/upload", + files={"image": f}, + data={"date": time.time()}, + ) + response.data = response.json() + assert response.status_code == 200 + assert response.data["label"] == "epaule_a_levier_sous_garde" + assert response.data["confidence"] == pytest.approx(1, 0.1) + assert response.data["confidence_level"] == "high" + assert response.data["gun_length"] is not None + assert response.data["gun_barrel_length"] is not None + assert response.data["conf_card"] is not None \ No newline at end of file diff --git a/backend/tests/test_model.py b/backend/tests/test_model.py deleted file mode 100644 index c425e29c9..000000000 --- a/backend/tests/test_model.py +++ /dev/null @@ -1,13 +0,0 @@ -import os - -import pytest -from src.model import CLASSES, load_model_inference, predict_image -from src.main import app - -class TestModel: - def test_predict_image(self): - """Checks the prediction of an image by the model""" - with open("./tests/revolver.jpg", "rb") as f: - res = predict_image(app.model, f.read()) - assert res[0] == "revolver" - assert res[1] == pytest.approx(1, 0.1) diff --git a/docker-compose.yml b/docker-compose.yml index 61c43970f..fb245b81a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3.8' services: backend: build: @@ -9,7 +8,6 @@ services: - VERSION=${TAG:-latest} - CACERT_LOCATION context: ./backend - target: ${BUILD_TARGET:-dev} command: uvicorn src.main:app --reload --host 0.0.0.0 --port 5000 --no-server-header container_name: basegun-backend environment: @@ -20,15 +18,13 @@ services: - http_proxy - https_proxy - no_proxy - - WORKSPACE=dev - REQUESTS_CA_BUNDLE=$CACERT_LOCATION ports: - 5000:5000 volumes: - - $PWD/backend/src:/app/src - - $PWD/backend/tests:/app/tests - - $PWD/backend/logs:/tmp/logs - - /app/src/weights + - ./backend/src:/app/src + - ./backend/tests:/app/tests + - ./backend/logs:/tmp/logs frontend: build: @@ -44,7 +40,7 @@ services: - 8080:80 # if BUILD_TARGET = prod - 3000:5173 volumes: - - $PWD/frontend/src:/app/src + - ./frontend/src:/app/src - /app/node_modules minio: diff --git a/frontend/.eslintrc-auto-import.json b/frontend/.eslintrc-auto-import.json index 8790f5b6a..85f45d949 100644 --- a/frontend/.eslintrc-auto-import.json +++ b/frontend/.eslintrc-auto-import.json @@ -238,7 +238,6 @@ "useSpeechRecognition": true, "useSpeechSynthesis": true, "useStepper": true, - "useStepsStore": true, "useStorage": true, "useStorageAsync": true, "useStyleTag": true, @@ -296,8 +295,6 @@ "injectLocal": true, "provideLocal": true, "useClipboardItems": true, - "OhVueIcon": true, - "addIcons": true, "useScheme": true, "useTabs": true } diff --git a/frontend/cypress.config.cjs b/frontend/cypress.config.cjs index c457b36b9..f9ab81447 100644 --- a/frontend/cypress.config.cjs +++ b/frontend/cypress.config.cjs @@ -1,7 +1,7 @@ const { defineConfig } = require('cypress') const frontendHost = process.env.FRONTEND_HOST || 'localhost' -const frontendPort = process.env.FRONTEND_PORT || '5173' +const frontendPort = process.env.FRONTEND_PORT || '3000' module.exports = defineConfig({ e2e: { diff --git a/frontend/cypress/e2e/firearm-fiability.cy.js b/frontend/cypress/e2e/firearm-fiability.cy.js index 4fe457306..5415c9b77 100644 --- a/frontend/cypress/e2e/firearm-fiability.cy.js +++ b/frontend/cypress/e2e/firearm-fiability.cy.js @@ -1,12 +1,6 @@ describe('Firearm Fiability', () => { - it.skip('should identificate firearm with high fiability', () => { - cy.accueil() - cy.getByDataTestid('identification') - .contains('J’ai déjà mis mon arme en sécurité, je veux l’identifier') - .click() - cy.url().should('contain', '/instructions') - cy.contains('h3', 'Pour un résultat optimal') - cy.contains('span', 'canon vers la droite') + it('should identificate firearm with high fiability', () => { + cy.Identification() cy.getByDataTestid('select-file').as('fileInput') cy.intercept('POST', '/api/upload').as('upload') @@ -15,14 +9,7 @@ describe('Firearm Fiability', () => { expect(response.statusCode).to.eq(200) }) cy.getByDataTestid('next-step').click() - cy.url().should('contain', '/guide-identification/informations-complementaires') - cy.getByDataTestid('explanation').should('contain', 'questions supplémentaires') - cy.getByDataTestid('next-step').click() - cy.url().should('contain', '/guide-identification/munition-type') - cy.getByDataTestid('next-step').should('have.attr', 'disabled') - cy.contains('cartouches').first().click() - cy.getByDataTestid('next-step').should('not.have.attr', 'disabled') - cy.getByDataTestid('next-step').click() + cy.IdentificationPistoletSemiAuto() cy.url().should('contain', '/guide-identification/resultat-final') cy.getByDataTestid('arm-category').should('contain', 'Catégorie B') cy.getByDataTestid('arm-category').should(() => { @@ -30,45 +17,33 @@ describe('Firearm Fiability', () => { }) }) - it.skip('should identificate firearm with medium fiability', () => { - cy.accueil() - cy.getByDataTestid('identification') - .contains('J’ai déjà mis mon arme en sécurité, je veux l’identifier') - .click() - cy.url().should('contain', '/instructions') - cy.contains('h3', 'Pour un résultat optimal') - cy.contains('span', 'canon vers la droite') + it('should identificate firearm with medium fiability', () => { + cy.Identification() cy.getByDataTestid('select-file').as('fileInput') cy.intercept('POST', '/api/upload').as('upload') - cy.get('@fileInput').selectFile('./cypress/images/arme-medium.jpg', { force: true }) + cy.get('@fileInput').selectFile('./cypress/images/arme-medium.png', { force: true }) cy.wait('@upload').then(({ response }) => { expect(response.statusCode).to.eq(200) }) cy.url().should('contain', '/guide-identification/resultat-typologie') - cy.contains('p', 'Arme semi-automatique ou automatique') + cy.contains('h3', 'Catégorie A, B ou D') cy.get('h2').should(() => { expect(localStorage.getItem('confidenceLevel')).to.eq('"medium"') }) }) - it.skip('should identificate firearm with low fiability', () => { - cy.accueil() - cy.getByDataTestid('identification') - .contains('J’ai déjà mis mon arme en sécurité, je veux l’identifier') - .click() - cy.url().should('contain', '/instructions') - cy.contains('h3', 'Pour un résultat optimal') - cy.contains('span', 'canon vers la droite') + it('should identificate firearm with low fiability', () => { + cy.Identification() cy.getByDataTestid('select-file').as('fileInput') cy.intercept('POST', '/api/upload').as('upload') - cy.get('@fileInput').selectFile('./cypress/images/arme-low.jpg', { force: true }) + cy.get('@fileInput').selectFile('./cypress/images/arme-low.png', { force: true }) cy.wait('@upload').then(({ response }) => { expect(response.statusCode).to.eq(200) }) cy.url().should('contain', '/guide-identification/resultat-typologie') - cy.contains('p', 'Catégorie Non déterminée') + cy.contains('h2', 'Catégorie non déterminée') cy.get('h2').should(() => { expect(localStorage.getItem('confidenceLevel')).to.eq('"low"') }) diff --git a/frontend/cypress/e2e/firearm-identification.cy.js b/frontend/cypress/e2e/firearm-identification.cy.js index fba5e0d74..198b5799e 100644 --- a/frontend/cypress/e2e/firearm-identification.cy.js +++ b/frontend/cypress/e2e/firearm-identification.cy.js @@ -1,13 +1,6 @@ describe('Firearm Identification', () => { it('should identificate real firearm', () => { - cy.accueil() - cy.getByDataTestid('identification') - .contains('J’ai déjà mis mon arme en sécurité, je veux l’identifier') - .click() - cy.url().should('contain', '/instructions') - cy.contains('h3', 'Pour un résultat optimal') - cy.contains('span', 'canon vers la droite') - + cy.Identification() cy.getByDataTestid('select-file').as('fileInput') cy.intercept('POST', '/api/upload').as('upload') cy.get('@fileInput').selectFile('./cypress/images/pistolet-semi-auto.jpg', { force: true }) @@ -15,14 +8,7 @@ describe('Firearm Identification', () => { expect(response.statusCode).to.eq(200) }) cy.getByDataTestid('next-step').click() - cy.url().should('contain', '/guide-identification/informations-complementaires') - cy.getByDataTestid('explanation').should('contain', 'questions supplémentaires') - cy.getByDataTestid('next-step').click() - cy.url().should('contain', '/guide-identification/munition-type') - cy.getByDataTestid('next-step').should('have.attr', 'disabled') - cy.contains('Cartouches').first().click() - cy.getByDataTestid('next-step').should('not.have.attr', 'disabled') - cy.getByDataTestid('next-step').click() + cy.IdentificationPistoletSemiAuto() cy.url().should('contain', '/guide-identification/resultat-final') cy.getByDataTestid('arm-category').should('contain', 'Catégorie B') cy.getByDataTestid('return-to-home-end').click() @@ -30,14 +16,7 @@ describe('Firearm Identification', () => { }) it('should identificate dummy firearm', () => { - cy.accueil() - cy.getByDataTestid('identification') - .contains('J’ai déjà mis mon arme en sécurité, je veux l’identifier') - .click() - cy.url().should('contain', '/instructions') - cy.contains('h3', 'Pour un résultat optimal') - cy.contains('span', 'canon vers la droite') - + cy.Identification() cy.getByDataTestid('select-file').as('fileInput') cy.intercept('POST', '/api/upload').as('upload') cy.get('@fileInput').selectFile('./cypress/images/pistolet-semi-auto.jpg', { force: true }) @@ -45,16 +24,7 @@ describe('Firearm Identification', () => { expect(response.statusCode).to.eq(200) }) cy.getByDataTestid('next-step').click() - cy.url().should('contain', '/guide-identification/informations-complementaires') - cy.getByDataTestid('explanation').should('contain', 'questions supplémentaires') - cy.getByDataTestid('next-step').click() - cy.getByDataTestid('next-step').should('have.attr', 'disabled') - cy.contains('Billes').first().click() - cy.url().should('contain', '/guide-identification/munition-type') - cy.getByDataTestid('next-step').should('not.have.attr', 'disabled') - cy.getByDataTestid('next-step').click() - cy.url().should('contain', '/guide-identification/resultat-final') - cy.getByDataTestid('arm-category').should('contain', 'Catégorie Non Classée') + cy.IdentificationDummyPistolet() cy.getByDataTestid('return-to-home-end').click() cy.url().should('contain', '/accueil') }) diff --git a/frontend/cypress/e2e/firearm-securing.cy.js b/frontend/cypress/e2e/firearm-securing.cy.js index acadd7703..4f1a57fc3 100644 --- a/frontend/cypress/e2e/firearm-securing.cy.js +++ b/frontend/cypress/e2e/firearm-securing.cy.js @@ -1,6 +1,5 @@ describe('Securing Firearm and Identification', () => { it('should secure and identificate real firearm', () => { - cy.accueil() cy.miseEnSecurite() cy.getByDataTestid('select-file').as('fileInput') cy.intercept('POST', '/api/upload').as('upload') @@ -15,13 +14,16 @@ describe('Securing Firearm and Identification', () => { cy.getByDataTestid('button-next').click() cy.url().should('contain', '/mise-en-securite-tutoriel') cy.getVideo() - cy.contains('h2', 'Mettre en sécurité mon arme') + cy.contains('h1', 'Mettre en sécurité mon arme') cy.contains('li', 'Actionner la culasse') cy.getByDataTestid('button-next').click() - cy.Identification() - cy.contains('Cartouches').first().click() - cy.getByDataTestid('next-step').should('not.have.attr', 'disabled') + cy.contains('h1', 'Fin de la mise en sécurité') + cy.getByDataTestid('go-to-identification').click() + cy.url().should('contain', '/guide-identification/resultat-typologie') + cy.contains('h1', 'Typologie de l\'arme') + cy.contains('p', 'Basegun a identifié votre arme') cy.getByDataTestid('next-step').click() + cy.IdentificationPistoletSemiAuto() cy.url().should('contain', '/guide-identification/resultat-final') cy.getByDataTestid('arm-category').should('contain', 'Catégorie B') cy.getByDataTestid('return-to-home-end').click() diff --git a/frontend/cypress/e2e/home.cy.js b/frontend/cypress/e2e/home.cy.js index 7ac546a9f..c0a7d27be 100644 --- a/frontend/cypress/e2e/home.cy.js +++ b/frontend/cypress/e2e/home.cy.js @@ -42,6 +42,15 @@ describe('HomePage', () => { cy.getByRole('navigation') .contains('a', 'Important') .click({ force: true }) + + cy.get('#button-menu') + .click() + cy.getByRole('navigation') + .contains('a', 'Accessibilité : partiellement conforme') + .click() + cy.url() + .should('contain', '/accessibilite') + cy.contains('h1', 'Déclaration d’accessibilité') }) }, ) diff --git a/frontend/cypress/e2e/old-mechanism-pistol-securing.cy.js b/frontend/cypress/e2e/old-mechanism-pistol-securing.cy.js index 2be7676a5..4f826eb8e 100644 --- a/frontend/cypress/e2e/old-mechanism-pistol-securing.cy.js +++ b/frontend/cypress/e2e/old-mechanism-pistol-securing.cy.js @@ -1,6 +1,5 @@ describe('Old Mechanism Pistol Securing', () => { it('should secure and identificate old mechanism pistol', () => { - cy.accueil() cy.miseEnSecurite() cy.getByDataTestid('select-file').as('fileInput') cy.intercept('POST', '/api/upload').as('upload') diff --git a/frontend/cypress/e2e/recommendations-civilians-vs-fsi.cy.js b/frontend/cypress/e2e/recommendations-civilians-vs-fsi.cy.js index 109afa77c..2600c97ba 100644 --- a/frontend/cypress/e2e/recommendations-civilians-vs-fsi.cy.js +++ b/frontend/cypress/e2e/recommendations-civilians-vs-fsi.cy.js @@ -1,13 +1,18 @@ describe('Recommendations Civilians vs FSI', () => { it('should show application version FSI', () => { + cy.visit('/', { + onBeforeLoad: win => { + Object.defineProperty(win.navigator, 'userAgent', { + value: 'SAID', + }) + }, + }) cy.accueil() cy.getByDataTestid('secure-firearm') .contains('Je veux mettre en sécurité mon arme') .click() - cy.url().should('contain', '/guide-mise-en-securite/mise-en-securite-recommandations') - cy.contains('h2', 'Mettre en sécurité mon arme') - cy.contains('span', 'extraire des munitions') - cy.get('h2') + cy.contains('h1', 'Mettre en sécurité mon arme') + cy.contains('p', 'En cas de doute,') }) it('should show application version Civilians', () => { @@ -18,17 +23,11 @@ describe('Recommendations Civilians vs FSI', () => { }) }, }) - cy.getByDataTestid('basegun-logo').should('exist') - cy.contains('li', 'Basegun est une application') - cy.get('swiper-container').shadow().find('.swiper-button-next').click() - cy.contains('li', 'ne remplace en aucun cas l\'avis d\'un expert') - cy.get('#agree-button').contains('J\'ai compris').click() - cy.url().should('contain', '/accueil') + cy.accueil() cy.getByDataTestid('secure-firearm') .contains('Je veux mettre en sécurité mon arme') .click() - cy.url().should('contain', '/guide-mise-en-securite/mise-en-securite-recommandations') - cy.contains('h2', 'Mettre en sécurité mon arme') + cy.contains('h1', 'Mettre en sécurité mon arme') cy.contains('span', 'extraire des munitions') }) }) diff --git a/frontend/cypress/e2e/shoulder-bolt-rifle-securing.cy.js b/frontend/cypress/e2e/shoulder-bolt-rifle-securing.cy.js index 457b144c7..3d2605b72 100644 --- a/frontend/cypress/e2e/shoulder-bolt-rifle-securing.cy.js +++ b/frontend/cypress/e2e/shoulder-bolt-rifle-securing.cy.js @@ -1,6 +1,5 @@ describe('Shoulder Bolt Rifle Securing', () => { it('should secure and identficate real shoulder bolt rifle', () => { - cy.accueil() cy.miseEnSecurite() cy.getByDataTestid('select-file').as('fileInput') cy.intercept('POST', '/api/upload').as('upload') @@ -10,15 +9,12 @@ describe('Shoulder Bolt Rifle Securing', () => { }) cy.getVideo() cy.url().should('contain', '/mise-en-securite-tutoriel') - cy.contains('h2', 'Mettre en sécurité mon arme') + cy.contains('h1', 'Mettre en sécurité mon arme') cy.contains('li', 'Ouvrez la culasse') cy.getByDataTestid('button-next').click() - cy.Identification() - cy.contains('Balles').first().click() - cy.getByDataTestid('next-step').should('not.have.attr', 'disabled') - cy.getByDataTestid('next-step').click() + cy.IdentificationShoulderBoltRifle() cy.url().should('contain', '/guide-identification/resultat-final') - cy.getByDataTestid('arm-category').should('contain', 'Catégorie B ou C') + cy.getByDataTestid('arm-category').should('contain', 'Catégorie C') cy.getByDataTestid('return-to-home-end').click() cy.url().should('contain', '/accueil') }) diff --git a/frontend/cypress/e2e/typology-revolver-identification.cy.js b/frontend/cypress/e2e/typology-revolver-identification.cy.js index 7cfa5cf04..6e658a8d7 100644 --- a/frontend/cypress/e2e/typology-revolver-identification.cy.js +++ b/frontend/cypress/e2e/typology-revolver-identification.cy.js @@ -1,28 +1,13 @@ describe('Typology Revolver Identification', () => { it('should identificate revolver typology', () => { - cy.accueil() - cy.getByDataTestid('identification') - .contains('J’ai déjà mis mon arme en sécurité, je veux l’identifier') - .click() - cy.url().should('contain', '/instructions') - cy.contains('h3', 'Pour un résultat optimal') - cy.contains('span', 'canon vers la droite') - + cy.Identification() cy.getByDataTestid('select-file').as('fileInput') cy.intercept('POST', '/api/upload').as('upload') cy.get('@fileInput').selectFile('./cypress/images/revolver.jpg', { force: true }) cy.wait('@upload').then(({ response }) => { expect(response.statusCode).to.eq(200) }) - cy.getByDataTestid('next-step').click() - cy.url().should('contain', '/guide-identification/informations-complementaires') - cy.getByDataTestid('explanation').should('contain', 'questions supplémentaires') - cy.getByDataTestid('next-step').click() - cy.url().should('contain', '/guide-identification/munition-type') - cy.getByDataTestid('next-step').should('have.attr', 'disabled') - cy.contains('Balles').first().click() - cy.getByDataTestid('next-step').should('not.have.attr', 'disabled') - cy.getByDataTestid('next-step').click() + cy.IdentificationRevolver() cy.url().should('contain', '/guide-identification/resultat-final') cy.getByDataTestid('arm-category').should('contain', 'Catégorie B ou D') cy.getByDataTestid('return-to-home-end').click() diff --git a/frontend/cypress/e2e/typology-revolver-securing.cy.js b/frontend/cypress/e2e/typology-revolver-securing.cy.js index 20f5dca9b..ec1ac1891 100644 --- a/frontend/cypress/e2e/typology-revolver-securing.cy.js +++ b/frontend/cypress/e2e/typology-revolver-securing.cy.js @@ -1,6 +1,5 @@ describe('Typology Revolver Securing', () => { it('should identificate revolver with small fireplaces (?) ', () => { - cy.accueil() cy.miseEnSecurite() cy.getByDataTestid('select-file').as('fileInput') cy.intercept('POST', '/api/upload').as('upload') @@ -14,15 +13,14 @@ describe('Typology Revolver Securing', () => { cy.getByDataTestid('button-next').should('not.have.attr', 'disabled') cy.getByDataTestid('button-next').click() cy.url().should('contain', '/fin-mise-en-securite') - cy.contains('h2', 'mise en sécurité') + cy.contains('h1', 'mise en sécurité') cy.contains('p', 'les manipulations sont complexes') cy.getByDataTestid('go-to-identification').click() - cy.url().should('contain', '/guide-identification/resultat-typologie') + cy.url().should('contain', '/guide-identification/resultat-final') cy.getByDataTestid('arm-category').should('contain', 'Catégorie D') }) it('should secure and identificate real revolver with barrel button', () => { - cy.accueil() cy.miseEnSecurite() cy.getByDataTestid('select-file').as('fileInput') cy.intercept('POST', '/api/upload').as('upload') @@ -30,31 +28,23 @@ describe('Typology Revolver Securing', () => { cy.wait('@upload').then(({ response }) => { expect(response.statusCode).to.eq(200) }) - cy.url().should('contain', '/mise-en-securite-choix-option-etape/1') - cy.getByDataTestid('button-next').should('have.attr', 'disabled') - cy.contains('Arrière plat').first().click() - cy.getByDataTestid('button-next').should('not.have.attr', 'disabled') - cy.getByDataTestid('button-next').click() - cy.url().should('contain', '/mise-en-securite-choix-option-etape/2') - cy.getByDataTestid('button-next').should('have.attr', 'disabled') + cy.arrierePlatRevolver() cy.contains('Bouton à côté du barillet').first().click() cy.getByDataTestid('button-next').should('not.have.attr', 'disabled') cy.getByDataTestid('button-next').click() cy.getVideo() - cy.contains('h2', 'Mettre en sécurité mon arme') + cy.contains('h1', 'Mettre en sécurité mon arme') cy.contains('li', 'Tirer ou pousser') cy.getByDataTestid('button-next').click() cy.url().should('contain', '/fin-mise-en-securite') - cy.Identification() - cy.contains('Balles').first().click() - cy.getByDataTestid('next-step').click() + cy.getByDataTestid('go-to-identification').click() + cy.IdentificationRevolver() cy.url().should('contain', '/guide-identification/resultat-final') cy.getByDataTestid('arm-category').should('contain', 'Catégorie B') cy.getByDataTestid('return-to-home-end').click() cy.url().should('contain', '/accueil') }) it('should secure and identificate real revolver with hidden door', () => { - cy.accueil() cy.miseEnSecurite() cy.getByDataTestid('select-file').as('fileInput') cy.intercept('POST', '/api/upload').as('upload') @@ -62,13 +52,7 @@ describe('Typology Revolver Securing', () => { cy.wait('@upload').then(({ response }) => { expect(response.statusCode).to.eq(200) }) - cy.url().should('contain', '/mise-en-securite-choix-option-etape/1') - cy.getByDataTestid('button-next').should('have.attr', 'disabled') - cy.contains('Arrière plat').first().click() - cy.getByDataTestid('button-next').should('not.have.attr', 'disabled') - cy.getByDataTestid('button-next').click() - cy.url().should('contain', '/mise-en-securite-choix-option-etape/2') - cy.getByDataTestid('button-next').should('have.attr', 'disabled') + cy.arrierePlatRevolver() cy.contains('Portière qui cache le côté droit du barillet').first().click() cy.getByDataTestid('button-next').should('not.have.attr', 'disabled') cy.getByDataTestid('button-next').click() @@ -76,13 +60,12 @@ describe('Typology Revolver Securing', () => { cy.contains('Le barillet ne bascule pas').first().click() cy.getByDataTestid('button-next').should('not.have.attr', 'disabled') cy.getByDataTestid('button-next').click() - cy.get('.fr-accordions-group > li').first().click() - cy.getByDataTestid('button-step-mes').click({ multiple: true }, { force: true }) - cy.getByDataTestid('button-next').should('not.have.attr', 'disabled') + cy.contains('h1', 'Mettre en sécurité mon arme') + cy.contains('li', 'Contrôler que chaque chambre') cy.getByDataTestid('button-next').click() - cy.Identification() - cy.contains('Balles').first().click() - cy.getByDataTestid('next-step').click() + cy.url().should('contain', '/fin-mise-en-securite') + cy.getByDataTestid('go-to-identification').click() + cy.IdentificationRevolver() cy.url().should('contain', '/guide-identification/resultat-final') cy.getByDataTestid('arm-category').should('contain', 'Catégorie B') cy.getByDataTestid('return-to-home-end').click() diff --git a/frontend/cypress/images/arme-low.jpg b/frontend/cypress/images/arme-low.jpg deleted file mode 100644 index 498efe2bc..000000000 Binary files a/frontend/cypress/images/arme-low.jpg and /dev/null differ diff --git a/frontend/cypress/images/arme-low.png b/frontend/cypress/images/arme-low.png new file mode 100644 index 000000000..4411e77ad Binary files /dev/null and b/frontend/cypress/images/arme-low.png differ diff --git a/frontend/cypress/images/arme-medium.jpg b/frontend/cypress/images/arme-medium.jpg deleted file mode 100644 index 91df71753..000000000 Binary files a/frontend/cypress/images/arme-medium.jpg and /dev/null differ diff --git a/frontend/cypress/images/arme-medium.png b/frontend/cypress/images/arme-medium.png new file mode 100644 index 000000000..3dc108c5e Binary files /dev/null and b/frontend/cypress/images/arme-medium.png differ diff --git a/frontend/cypress/support/commands.js b/frontend/cypress/support/commands.js index b412f7dec..a78caadf4 100644 --- a/frontend/cypress/support/commands.js +++ b/frontend/cypress/support/commands.js @@ -55,7 +55,6 @@ Cypress.Commands.add('getVideo', () => { }) Cypress.Commands.add('accueil', () => { - cy.visit('/') cy.getByDataTestid('basegun-logo').should('exist') cy.contains('li', 'Basegun est une application') cy.get('swiper-container').shadow().find('.swiper-button-next').click() @@ -65,23 +64,27 @@ Cypress.Commands.add('accueil', () => { }) Cypress.Commands.add('miseEnSecurite', () => { + cy.visit('/') + cy.getByDataTestid('basegun-logo').should('exist') + cy.contains('li', 'Basegun est une application') + cy.get('swiper-container').shadow().find('.swiper-button-next').click() + cy.contains('li', 'ne remplace en aucun cas l\'avis d\'un expert') + cy.get('#agree-button').contains('J\'ai compris').click() + cy.url().should('contain', '/accueil') cy.getByDataTestid('secure-firearm') .contains('Je veux mettre en sécurité mon arme') .click() - cy.url().should('contain', '/guide-mise-en-securite/mise-en-securite-recommandations') - cy.contains('h2', 'Mettre en sécurité mon arme') + cy.contains('h1', 'Mettre en sécurité mon arme') cy.contains('span', 'extraire des munitions') cy.getByDataTestid('button-next') .contains('Suivant') .click() - cy.url().should('contain', '/guide-mise-en-securite/mise-en-securite-instructions') - cy.contains('h2', 'Mettre en sécurité mon arme') + cy.contains('h1', 'Mettre en sécurité mon arme') cy.contains('span', 'DIRECTION SÛRE') cy.getByDataTestid('button-next') .contains('Suivant') .click() - cy.url().should('contain', '/guide-mise-en-securite/mise-en-securite-introduction') - cy.contains('h2', 'Mettre en sécurité mon arme') + cy.contains('h1', 'Mettre en sécurité mon arme') cy.contains('span', 'tutoriel adapté') cy.getByDataTestid('button-next') .contains('Suivant') @@ -89,20 +92,94 @@ Cypress.Commands.add('miseEnSecurite', () => { }) Cypress.Commands.add('Identification', () => { - cy.contains('h2', 'Fin de la mise en sécurité') + cy.visit('/') + cy.getByDataTestid('basegun-logo').should('exist') + cy.contains('li', 'Basegun est une application') + cy.get('swiper-container').shadow().find('.swiper-button-next').click() + cy.contains('li', 'ne remplace en aucun cas l\'avis d\'un expert') + cy.get('#agree-button').contains('J\'ai compris').click() + cy.url().should('contain', '/accueil') + cy.getByDataTestid('identification') + .contains('J’ai déjà mis mon arme en sécurité, je veux l’identifier') + .click() + cy.url().should('contain', '/instructions') + cy.contains('h1', 'Pour un résultat optimal') + cy.contains('span', 'canon vers la droite') +}) + +Cypress.Commands.add('IdentificationPistoletSemiAuto', () => { + cy.url().should('contain', 'guide-identification/informations-complementaires') + cy.getByDataTestid('next-step').click() + cy.url().should('contain', '/guide-identification/munition-type') + cy.getByDataTestid('next-step').should('have.attr', 'disabled') + cy.contains('Cartouches').first().click() + cy.getByDataTestid('next-step').should('not.have.attr', 'disabled') + cy.getByDataTestid('next-step').click() + cy.url().should('contain', '/guide-identification/armes-alarme') + cy.getByDataTestid('instruction-armeAlarme').should('contain', 'Votre arme') + cy.getByDataTestid('next-step').click() + cy.getByDataTestid('aucune-correspondance').click() + cy.getByDataTestid('next-step').click() +}) + +Cypress.Commands.add('IdentificationRevolver', () => { + cy.url().should('contain', '/resultat-typologie') + cy.getByDataTestid('next-step').click() + cy.url().should('contain', 'guide-identification/informations-complementaires') + cy.getByDataTestid('explanation').should('contain', 'questions supplémentaires') + cy.getByDataTestid('next-step').click() + cy.url().should('contain', '/guide-identification/munition-type') + cy.getByDataTestid('next-step').should('have.attr', 'disabled') + cy.contains('Balles').first().click() + cy.getByDataTestid('next-step').should('not.have.attr', 'disabled') + cy.getByDataTestid('next-step').click() + cy.url().should('contain', '/guide-identification/armes-alarme') + cy.getByDataTestid('instruction-armeAlarme').should('contain', 'Votre arme') + cy.getByDataTestid('next-step').click() + cy.getByDataTestid('aucune-correspondance').click() + cy.getByDataTestid('next-step').click() +}) + +Cypress.Commands.add('arrierePlatRevolver', () => { + cy.url().should('contain', '/mise-en-securite-choix-option-etape/1') + cy.getByDataTestid('button-next').should('have.attr', 'disabled') + cy.contains('Arrière plat').first().click() + cy.getByDataTestid('button-next').should('not.have.attr', 'disabled') + cy.getByDataTestid('button-next').click() + cy.url().should('contain', '/mise-en-securite-choix-option-etape/2') + cy.getByDataTestid('button-next').should('have.attr', 'disabled') +}) + +Cypress.Commands.add('IdentificationShoulderBoltRifle', () => { + cy.url().should('contain', '/fin-mise-en-securite') cy.getByDataTestid('go-to-identification').click() - cy.url().should('contain', '/guide-identification/resultat-typologie') - cy.contains('h2', 'Typologie de l\'arme') - cy.contains('p', 'Basegun a identifié votre arme') + cy.url().should('contain', '/resultat-typologie') cy.getByDataTestid('next-step').click() cy.url().should('contain', 'guide-identification/informations-complementaires') + cy.getByDataTestid('explanation').should('contain', 'questions supplémentaires') cy.getByDataTestid('next-step').click() cy.url().should('contain', '/guide-identification/munition-type') cy.getByDataTestid('next-step').should('have.attr', 'disabled') + cy.contains('Balles').first().click() + cy.getByDataTestid('next-step').should('not.have.attr', 'disabled') + cy.getByDataTestid('next-step').click() +}) + +Cypress.Commands.add('IdentificationDummyPistolet', () => { + cy.url().should('contain', '/guide-identification/informations-complementaires') + cy.getByDataTestid('explanation').should('contain', 'questions supplémentaires') + cy.getByDataTestid('next-step').click() + cy.getByDataTestid('next-step').should('have.attr', 'disabled') + cy.contains('Billes').first().click() + cy.url().should('contain', '/guide-identification/munition-type') + cy.getByDataTestid('next-step').should('not.have.attr', 'disabled') + cy.getByDataTestid('next-step').click() + cy.url().should('contain', '/guide-identification/resultat-final') + cy.getByDataTestid('arm-category').should('contain', 'Catégorie Non Classée') }) Cypress.Commands.add('pasDeGuide', () => { - cy.contains('h2', 'Pas de guide de mise en sécurité pour votre arme') + cy.contains('h1', 'Pas de guide de mise en sécurité pour votre arme') cy.url().should('contain', '/fin-mise-en-securite') cy.getByDataTestid('go-to-identification').click() cy.url().should('contain', '/guide-identification/resultat-typologie') diff --git a/frontend/index.html b/frontend/index.html index d93342679..a53b79424 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,5 +1,5 @@ - + @@ -8,7 +8,7 @@ Basegun - + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d16ff3e44..1699b1441 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "@gouvminint/vue-dsfr": "^5.8.0", "@vueuse/core": "^10.7.2", "axios": "^1.6.7", + "luxon": "^3.4.4", "pinia": "^2.1.7", "stylelint-config-recommended-vue": "^1.5.0", "swiper": "^11.0.6", @@ -30,7 +31,7 @@ "@vitejs/plugin-vue": "^5.0.3", "@vue/eslint-config-typescript": "^12.0.0", "@vue/tsconfig": "^0.5.1", - "cypress": "^13.6.4", + "cypress": "^13.7.1", "eslint": "^8.56.0", "eslint-config-standard": "^17.1.0", "eslint-config-standard-with-typescript": "^43.0.1", @@ -3288,6 +3289,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "dev": true + }, "node_modules/@types/node": { "version": "20.11.16", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", @@ -5648,9 +5655,9 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/cypress": { - "version": "13.6.4", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.4.tgz", - "integrity": "sha512-pYJjCfDYB+hoOoZuhysbbYhEmNW7DEDsqn+ToCLwuVowxUXppIWRr7qk4TVRIU471ksfzyZcH+mkoF0CQUKnpw==", + "version": "13.7.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.7.1.tgz", + "integrity": "sha512-4u/rpFNxOFCoFX/Z5h+uwlkBO4mWzAjveURi3vqdSu56HPvVdyGTxGw4XKGWt399Y1JwIn9E1L9uMXQpc0o55w==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -5661,7 +5668,7 @@ "arch": "^2.2.0", "blob-util": "^2.0.2", "bluebird": "^3.7.2", - "buffer": "^5.6.0", + "buffer": "^5.7.1", "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", @@ -5679,7 +5686,7 @@ "figures": "^3.2.0", "fs-extra": "^9.1.0", "getos": "^3.2.1", - "is-ci": "^3.0.0", + "is-ci": "^3.0.1", "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", "listr2": "^3.8.3", @@ -8505,6 +8512,14 @@ "node": ">=10" } }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -14540,6 +14555,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "dev": true + }, "@types/node": { "version": "20.11.16", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", @@ -16218,9 +16239,9 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "cypress": { - "version": "13.6.4", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.4.tgz", - "integrity": "sha512-pYJjCfDYB+hoOoZuhysbbYhEmNW7DEDsqn+ToCLwuVowxUXppIWRr7qk4TVRIU471ksfzyZcH+mkoF0CQUKnpw==", + "version": "13.7.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.7.1.tgz", + "integrity": "sha512-4u/rpFNxOFCoFX/Z5h+uwlkBO4mWzAjveURi3vqdSu56HPvVdyGTxGw4XKGWt399Y1JwIn9E1L9uMXQpc0o55w==", "dev": true, "requires": { "@cypress/request": "^3.0.0", @@ -16230,7 +16251,7 @@ "arch": "^2.2.0", "blob-util": "^2.0.2", "bluebird": "^3.7.2", - "buffer": "^5.6.0", + "buffer": "^5.7.1", "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", @@ -16248,7 +16269,7 @@ "figures": "^3.2.0", "fs-extra": "^9.1.0", "getos": "^3.2.1", - "is-ci": "^3.0.0", + "is-ci": "^3.0.1", "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", "listr2": "^3.8.3", @@ -18319,6 +18340,11 @@ "yallist": "^4.0.0" } }, + "luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==" + }, "lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index dd199ece2..d5d21c61b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,14 +14,14 @@ "format:css": "stylelint --fix src/**/*.{css,vue}", "pretest:unit": "npm run lint", "test:e2e-ci": "FRONTEND_HOST=localhost FRONTEND_PORT=3000 cypress run --e2e --browser firefox", - "test:e2e-open": "FRONTEND_HOST=localhost FRONTEND_PORT=5173 cypress open --e2e --browser firefox", - "test:e2e-run": "FRONTEND_HOST=localhost FRONTEND_PORT=3000 cypress run --e2e --browser firefox" + "test:e2e-open": "FRONTEND_HOST=localhost FRONTEND_PORT=5173 cypress open --e2e --browser firefox" }, "dependencies": { "@gouvfr/dsfr": "~1.11.0", "@gouvminint/vue-dsfr": "^5.8.0", "@vueuse/core": "^10.7.2", "axios": "^1.6.7", + "luxon": "^3.4.4", "pinia": "^2.1.7", "stylelint-config-recommended-vue": "^1.5.0", "swiper": "^11.0.6", @@ -40,7 +40,7 @@ "@vitejs/plugin-vue": "^5.0.3", "@vue/eslint-config-typescript": "^12.0.0", "@vue/tsconfig": "^0.5.1", - "cypress": "^13.6.4", + "cypress": "^13.7.1", "eslint": "^8.56.0", "eslint-config-standard": "^17.1.0", "eslint-config-standard-with-typescript": "^43.0.1", diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png index 2948a7e52..d0aedf609 100644 Binary files a/frontend/public/apple-touch-icon.png and b/frontend/public/apple-touch-icon.png differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico index df5ecf937..e6d18fa4f 100644 Binary files a/frontend/public/favicon.ico and b/frontend/public/favicon.ico differ diff --git a/frontend/public/pwa-192x192.png b/frontend/public/pwa-192x192.png index 9f7a3cf54..1932b1bd8 100644 Binary files a/frontend/public/pwa-192x192.png and b/frontend/public/pwa-192x192.png differ diff --git a/frontend/public/pwa-512x512.png b/frontend/public/pwa-512x512.png index 1b64626e1..4a90419f6 100644 Binary files a/frontend/public/pwa-512x512.png and b/frontend/public/pwa-512x512.png differ diff --git a/frontend/src/assets/basegun.png b/frontend/src/assets/basegun.png index b88e612d2..d794ca290 100644 Binary files a/frontend/src/assets/basegun.png and b/frontend/src/assets/basegun.png differ diff --git a/frontend/src/assets/basegun_green.png b/frontend/src/assets/basegun_green.png index fd6775654..0c3e93446 100644 Binary files a/frontend/src/assets/basegun_green.png and b/frontend/src/assets/basegun_green.png differ diff --git a/frontend/src/assets/basegun_long.png b/frontend/src/assets/basegun_long.png new file mode 100644 index 000000000..757c836cb Binary files /dev/null and b/frontend/src/assets/basegun_long.png differ diff --git a/frontend/src/assets/basegun_red.png b/frontend/src/assets/basegun_red.png deleted file mode 100644 index 9abec5547..000000000 Binary files a/frontend/src/assets/basegun_red.png and /dev/null differ diff --git a/frontend/src/assets/good-photo-example.jpg b/frontend/src/assets/good-photo-example.jpg deleted file mode 100644 index f0ca6ab74..000000000 Binary files a/frontend/src/assets/good-photo-example.jpg and /dev/null differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_C75.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_C75.jpg new file mode 100644 index 000000000..eb71d0b8a Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_C75.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_C75_Zoom.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_C75_Zoom.jpg new file mode 100644 index 000000000..3e6fc604b Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_C75_Zoom.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_F92.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_F92.jpg new file mode 100644 index 000000000..936f38fca Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_F92.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_F92_AUTO.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_F92_AUTO.jpg new file mode 100644 index 000000000..62444dd26 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_F92_AUTO.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_F92_AUTO_Zoom.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_F92_AUTO_Zoom.jpg new file mode 100644 index 000000000..1e1114217 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_F92_AUTO_Zoom.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_F92_Zoom.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_F92_Zoom.jpg new file mode 100644 index 000000000..fb614f5a9 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/BLOW_F92_Zoom.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_75_AUTO.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_75_AUTO.jpg new file mode 100644 index 000000000..27c005fd5 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_75_AUTO.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_75_AUTO_Zoom.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_75_AUTO_Zoom.jpg new file mode 100644 index 000000000..36e655040 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_75_AUTO_Zoom.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_911.png b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_911.png new file mode 100644 index 000000000..72c6961c4 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_911.png differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_911_Zoom.png b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_911_Zoom.png new file mode 100644 index 000000000..a8cffba48 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_911_Zoom.png differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_92_AUTO.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_92_AUTO.jpg new file mode 100644 index 000000000..3da599889 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_92_AUTO.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_92_AUTO_Zoom.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_92_AUTO_Zoom.jpg new file mode 100644 index 000000000..2cf2f4bbb Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_92_AUTO_Zoom.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_CA.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_CA.jpg new file mode 100644 index 000000000..413888c00 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_CA.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_CA_Zoom.png b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_CA_Zoom.png new file mode 100644 index 000000000..4a4b843e7 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_CA_Zoom.png differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_PA.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_PA.jpg new file mode 100644 index 000000000..fa79fee3f Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_PA.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_PA_Zoom.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_PA_Zoom.jpg new file mode 100644 index 000000000..f5fa298ad Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/KIMAR_PA_Zoom.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_2906.png b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_2906.png new file mode 100644 index 000000000..5e525c4c5 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_2906.png differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_2906_Zoom.png b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_2906_Zoom.png new file mode 100644 index 000000000..e5d8a895c Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_2906_Zoom.png differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_906.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_906.jpg new file mode 100644 index 000000000..d8a664392 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_906.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_906_Zoom.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_906_Zoom.jpg new file mode 100644 index 000000000..2ad6e8055 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_906_Zoom.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_918.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_918.jpg new file mode 100644 index 000000000..74c6938f4 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_918.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_918_Zoom.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_918_Zoom.jpg new file mode 100644 index 000000000..9c29b220e Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_918_Zoom.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_925.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_925.jpg new file mode 100644 index 000000000..aad691656 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_925.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_925_Zoom.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_925_Zoom.jpg new file mode 100644 index 000000000..c50a531b6 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_925_Zoom.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_R2.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_R2.jpg new file mode 100644 index 000000000..6ead948ed Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_R2.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_R2_Zoom.jpg b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_R2_Zoom.jpg new file mode 100644 index 000000000..30b56aecb Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/ZORAKI_R2_Zoom.jpg differ diff --git a/frontend/src/assets/guide-identification/photos/arme_alarme/picto.png b/frontend/src/assets/guide-identification/photos/arme_alarme/picto.png new file mode 100644 index 000000000..ba1d7d2f5 Binary files /dev/null and b/frontend/src/assets/guide-identification/photos/arme_alarme/picto.png differ diff --git a/frontend/src/assets/guide-mise-en-securite/videos/epaule_a_pompe/epaule_pompe_complique.mp4 b/frontend/src/assets/guide-mise-en-securite/videos/epaule_a_pompe/epaule_pompe_complique.mp4 deleted file mode 100644 index 6944b587f..000000000 Binary files a/frontend/src/assets/guide-mise-en-securite/videos/epaule_a_pompe/epaule_pompe_complique.mp4 and /dev/null differ diff --git a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_FR_debut.mp4 b/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_FR_debut.mp4 new file mode 100644 index 000000000..cb92eada8 Binary files /dev/null and b/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_FR_debut.mp4 differ diff --git a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_FR_fin.mp4 b/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_FR_fin.mp4 new file mode 100644 index 000000000..ec7e49993 Binary files /dev/null and b/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_FR_fin.mp4 differ diff --git a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p1.mp4 b/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p1.mp4 deleted file mode 100644 index 3465feabe..000000000 Binary files a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p1.mp4 and /dev/null differ diff --git a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p2.mp4 b/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p2.mp4 deleted file mode 100644 index a41813610..000000000 Binary files a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p2.mp4 and /dev/null differ diff --git a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p3.mp4 b/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p3.mp4 deleted file mode 100644 index fa6a08e55..000000000 Binary files a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p3.mp4 and /dev/null differ diff --git a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p4.mp4 b/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p4.mp4 deleted file mode 100644 index a85127214..000000000 Binary files a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p4.mp4 and /dev/null differ diff --git a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p5.mp4 b/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p5.mp4 deleted file mode 100644 index c6de6eb75..000000000 Binary files a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p5.mp4 and /dev/null differ diff --git a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p6.mp4 b/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p6.mp4 deleted file mode 100644 index 8d73c107b..000000000 Binary files a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p6.mp4 and /dev/null differ diff --git a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p7.mp4 b/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p7.mp4 deleted file mode 100644 index f5831c736..000000000 Binary files a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1873_portiere_montage_p7.mp4 and /dev/null differ diff --git a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1892.mp4 b/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1892.mp4 index 27120bc93..02122920e 100644 Binary files a/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1892.mp4 and b/frontend/src/assets/guide-mise-en-securite/videos/revolver/revolver_1892.mp4 differ diff --git a/frontend/src/assets/instruction-screen-gun.webp b/frontend/src/assets/instruction-screen-gun.webp new file mode 100644 index 000000000..96a66bbfd Binary files /dev/null and b/frontend/src/assets/instruction-screen-gun.webp differ diff --git a/frontend/src/assets/logo_police.png b/frontend/src/assets/logo_police.png index 37aefaba9..70ff56df5 100644 Binary files a/frontend/src/assets/logo_police.png and b/frontend/src/assets/logo_police.png differ diff --git a/frontend/src/assets/missing_card.png b/frontend/src/assets/missing_card.png new file mode 100644 index 000000000..8fc18a43c Binary files /dev/null and b/frontend/src/assets/missing_card.png differ diff --git a/frontend/src/auto-imports.d.ts b/frontend/src/auto-imports.d.ts index 85e2547e8..d78c821a0 100644 --- a/frontend/src/auto-imports.d.ts +++ b/frontend/src/auto-imports.d.ts @@ -238,9 +238,9 @@ declare global { const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] const useStepper: typeof import('@vueuse/core')['useStepper'] - const useStepsStore: typeof import('./stores/steps')['useStepsStore'] const useStorage: typeof import('@vueuse/core')['useStorage'] const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] + const useStore: typeof import('./stores/result')['useStore'] const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] const useSupported: typeof import('@vueuse/core')['useSupported'] const useSwipe: typeof import('@vueuse/core')['useSwipe'] @@ -519,7 +519,6 @@ declare module 'vue' { readonly useRafFn: UnwrapRef readonly useRefHistory: UnwrapRef readonly useResizeObserver: UnwrapRef - readonly useResultStore: UnwrapRef readonly useRoute: UnwrapRef readonly useRouter: UnwrapRef readonly useScheme: UnwrapRef @@ -537,9 +536,9 @@ declare module 'vue' { readonly useSpeechRecognition: UnwrapRef readonly useSpeechSynthesis: UnwrapRef readonly useStepper: UnwrapRef - readonly useStepsStore: UnwrapRef readonly useStorage: UnwrapRef readonly useStorageAsync: UnwrapRef + readonly useStore: UnwrapRef readonly useStyleTag: UnwrapRef readonly useSupported: UnwrapRef readonly useSwipe: UnwrapRef @@ -811,7 +810,6 @@ declare module '@vue/runtime-core' { readonly useRafFn: UnwrapRef readonly useRefHistory: UnwrapRef readonly useResizeObserver: UnwrapRef - readonly useResultStore: UnwrapRef readonly useRoute: UnwrapRef readonly useRouter: UnwrapRef readonly useScheme: UnwrapRef @@ -829,9 +827,9 @@ declare module '@vue/runtime-core' { readonly useSpeechRecognition: UnwrapRef readonly useSpeechSynthesis: UnwrapRef readonly useStepper: UnwrapRef - readonly useStepsStore: UnwrapRef readonly useStorage: UnwrapRef readonly useStorageAsync: UnwrapRef + readonly useStore: UnwrapRef readonly useStyleTag: UnwrapRef readonly useSupported: UnwrapRef readonly useSwipe: UnwrapRef diff --git a/frontend/src/components.d.ts b/frontend/src/components.d.ts index c293803e6..0df3e8e6a 100644 --- a/frontend/src/components.d.ts +++ b/frontend/src/components.d.ts @@ -7,8 +7,11 @@ export {} declare module 'vue' { export interface GlobalComponents { + AccessibilityPage: typeof import('./components/AccessibilityPage.vue')['default'] AskingExpert: typeof import('./components/AskingExpert.vue')['default'] ContactExpert: typeof import('./components/ContactExpert.vue')['default'] + DsfrAccordion: typeof import('@gouvminint/vue-dsfr')['DsfrAccordion'] + DsfrAccordionsGroup: typeof import('@gouvminint/vue-dsfr')['DsfrAccordionsGroup'] DsfrAlert: typeof import('@gouvminint/vue-dsfr')['DsfrAlert'] DsfrButton: typeof import('@gouvminint/vue-dsfr')['DsfrButton'] DsfrCheckbox: typeof import('@gouvminint/vue-dsfr')['DsfrCheckbox'] @@ -18,10 +21,14 @@ declare module 'vue' { DsfrInputGroup: typeof import('@gouvminint/vue-dsfr')['DsfrInputGroup'] DsfrModal: typeof import('@gouvminint/vue-dsfr')['DsfrModal'] DsfrPicture: typeof import('@gouvminint/vue-dsfr')['DsfrPicture'] + DsfrRadioButton: typeof import('@gouvminint/vue-dsfr')['DsfrRadioButton'] + DsfrRadioButtonSet: typeof import('@gouvminint/vue-dsfr')['DsfrRadioButtonSet'] DsfrSelect: typeof import('@gouvminint/vue-dsfr')['DsfrSelect'] + DsfrTable: typeof import('@gouvminint/vue-dsfr')['DsfrTable'] DsfrTag: typeof import('@gouvminint/vue-dsfr')['DsfrTag'] FooterMES: typeof import('./components/FooterMES.vue')['default'] HeaderMain: typeof import('./components/HeaderMain.vue')['default'] + MissingCardAlert: typeof import('./components/MissingCardAlert.vue')['default'] OnboardingSwiper: typeof import('./components/OnboardingSwiper.vue')['default'] PopupContact: typeof import('./components/PopupContact.vue')['default'] PopupVideo: typeof import('./components/PopupVideo.vue')['default'] diff --git a/frontend/src/components/AskingExpert.vue b/frontend/src/components/AskingExpert.vue index 158265c86..c23551a24 100644 --- a/frontend/src/components/AskingExpert.vue +++ b/frontend/src/components/AskingExpert.vue @@ -3,65 +3,50 @@ import { ref, computed } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useSnackbarStore } from '@/stores/snackbar' -import { useStepsStore } from '@/stores/steps' -import { useResultStore } from '@/stores/result' +import { useStore } from '@/stores/result' import SnackbarAlert from '@/components/SnackbarAlert.vue' import { sendTutorialFeedback } from '@/api/api-client' const { setMessage } = useSnackbarStore() -const stepsStore = useStepsStore() -const resultStore = useResultStore() +const store = useStore() const router = useRouter() const route = useRoute() -const typology = computed(() => resultStore.typology) -const confidence = computed(() => resultStore.confidence) -const confidenceLevel = computed(() => resultStore.confidenceLevel) -const imgUrl = computed(() => resultStore.imgUrl) +const typology = computed(() => store.typology) +const confidence = computed(() => store.confidence) +const confidenceLevel = computed(() => store.confidenceLevel) +const imgUrl = computed(() => store.imgUrl) const showModal = ref(false) function onClose () { - stepsStore.tutorialFeedback = '' + store.tutorialFeedback = '' showModal.value = false } async function submitTutorialFeedback () { const feedback = { image_url: imgUrl.value, - tutorial_feedback: stepsStore.tutorialFeedback, + tutorial_feedback: store.tutorialFeedback, label: typology.value, - tutorial_option: stepsStore.currentOptionStep[stepsStore.currentStep] || null, + tutorial_option: store.currentOptionStep[store.currentStep] || null, route_name: route.name, confidence: confidence.value, confidence_level: confidenceLevel.value, } await axios.post('/tutorial-feedback', feedback) .then(async () => { - stepsStore.tutorialFeedback = feedback.tutorial_feedback + store.tutorialFeedback = feedback.tutorial_feedback setMessage({ type: 'success', message: 'Votre message a été pris en compte' }) }) .catch(async (err) => { import.meta.env.DEV && console.log(err) setMessage({ type: 'error', message: 'Une erreur a eu lieu en enregistrant de votre message.' }) }) - .finally(() => setTimeout(async () => { - stepsStore.setCurrentStep(1) - try { - await sendTutorialFeedback(feedback) - stepsStore.tutorialFeedback = feedback.tutorial_feedback - setMessage({ type: 'success', message: 'Votre message a été pris en compte' }) - } catch (err) { - import.meta.env.DEV && console.log(err) - setMessage({ type: 'error', message: 'Une erreur a eu lieu en enregistrant de votre message.' }) - } finally { - setTimeout(() => { - stepsStore.setCurrentStep(undefined) - stepsStore.tutorialFeedback = '' - router.push({ name: 'StartPage' }) - }, 3000) - } - })) + .finally(() => setTimeout(() => { + store.tutorialFeedback = '' + router.push({ name: 'ResultPage' }) + }, 3000)) }