Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
RouteController,
StudioController,
UserCollectionImportController,
UserCollectionImportHelpController,
UserController,
)
from services import (
Expand Down Expand Up @@ -116,6 +117,7 @@
auth_guard,
reset_job_coordinator=library_reset_job_coordinator,
)
user_collection_import_help_controller = UserCollectionImportHelpController(auth_guard)
library_service_provider = LibraryServiceProvider()
library_controller = LibraryController(
auth_guard,
Expand Down Expand Up @@ -159,6 +161,7 @@
collection_share_controller.register_routes(app)
user_controller.register_routes(app)
user_collection_import_controller.register_routes(app)
user_collection_import_help_controller.register_routes(app)
collection_controller.register_routes(app)
library_controller.register_routes(app)
platform_controller.register_routes(app)
Expand Down
2 changes: 2 additions & 0 deletions backend/controllers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .route_controller import RouteController
from .studio_controller import StudioController
from .user_collection_import_controller import UserCollectionImportController
from .user_collection_import_help_controller import UserCollectionImportHelpController
from .user_controller import UserController

__all__ = [
Expand All @@ -35,4 +36,5 @@
"StudioController",
"UserController",
"UserCollectionImportController",
"UserCollectionImportHelpController",
]
75 changes: 75 additions & 0 deletions backend/controllers/user_collection_import_help_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# ____ _ _ ____ _ _ _ _ ___
# / ___| | ___ _ _ __| |/ ___|___ | | | ___ ___| |_(_) ___ _ __ / _ \ _ __ _ __
# | | | |/ _ \| | | |/ _` | | / _ \| | |/ _ \/ __| __| |/ _ \| `_ \| | | | `_ \| `_ |
# | |___| | (_) | |_| | (_| | |__| (_) | | | __/ (__| |_| | (_) | | | | |_| | |_) | |_) |
# \____|_|\___/ \__,_|\__,_|\____\___|_|_|\___|\___|\__|_|\___/|_| |_|\___/| .__/| .__/
# |_| |_|
# Projet : CloudCollectionApp
# Date de creation : 2026-08-23
# Auteurs : OpenAI ChatGPT, Codex, Binda Sébastien
# Licence : Apache 2.0
#
# Description : controleur HTTP d'aide a la correction d'import.

from flask import Flask, jsonify, request

from services import AuthGuard, UserProfile
from services.collection.imports import CollectionImportInvalidValueHelpService


class UserCollectionImportHelpController:
"""Enregistre les routes d'aide liees aux erreurs d'import utilisateur."""

def __init__(
self,
auth_guard: AuthGuard,
invalid_value_help_service_class=CollectionImportInvalidValueHelpService,
):
"""Initialise le controleur d'aide d'import.

Args:
auth_guard (AuthGuard): Garde d'authentification et de profil.
invalid_value_help_service_class (type): Classe d'aide sur les valeurs refusees.

Returns:
None: Le constructeur ne retourne aucune valeur.
"""

self.auth_guard = auth_guard
self.invalid_value_help_service_class = invalid_value_help_service_class

def register_routes(self, flask_app: Flask) -> None:
"""Enregistre les routes d'aide de collection utilisateur dans Flask.

Args:
flask_app (Flask): Application Flask cible.

Returns:
None: La methode ne retourne aucune valeur.
"""

flask_app.add_url_rule(
"/api/users/import/invalid-value-help",
endpoint="get_import_invalid_value_help",
view_func=self.auth_guard.require_profile(UserProfile.USER.value)(
self.get_import_invalid_value_help
),
methods=["GET"],
)

def get_import_invalid_value_help(self):
"""Retourne l'aide de correction d'une valeur d'import refusee.

Args:
Aucun.

Returns:
tuple[flask.Response, int] | flask.Response: Aide JSON ou erreur.
"""

field = str(request.args.get("field") or "").strip()
value = str(request.args.get("value") or "").strip()
if not field:
return jsonify({"error": "Le parametre field est requis."}), 400
help_result = self.invalid_value_help_service_class().get_help(field, value)
return jsonify(help_result.to_dict()), 200
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
<p>Code raison: ${reason}</p>
<p>Jeux en erreur: ${invalid_games_count}/${total_games_count}</p>

<h2>Compteurs d'erreur</h2>
${error_counters}

<h2>Resultat de l'import</h2>
<ul>
<li>Plateformes rattachees: ${linked_platforms}</li>
Expand All @@ -31,9 +34,7 @@
<h2>Erreurs des jeux</h2>
${invalid_games}

<h2>Warnings</h2>
<pre>
${warnings}
</pre>
<h2>Plateformes à valider par l'admin</h2>
${manual_platform_mappings}
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
<li>Jeux en liste de souhaits: ${wishlisted_games}</li>
</ul>

<h2>Compteurs d'erreur</h2>
${error_counters}

<h2>Plateformes à valider par l'admin</h2>
${manual_platform_mappings}

<h2>Studios importes</h2>
${imported_studio_match_reports}

Expand All @@ -37,13 +43,8 @@ ${imported_game_match_reports}
</ul>

<h2>Configuration d'import</h2>
<pre>
<pre style="background:#f8fafc;border:1px solid #d9e2ec;border-radius:6px;color:#1f2933;font-family:Menlo,Consolas,monospace;font-size:13px;line-height:1.45;padding:12px;white-space:pre-wrap;">
${collection_file_description}
</pre>

<h2>Warnings</h2>
<pre>
${warnings}
</pre>
</body>
</html>
6 changes: 6 additions & 0 deletions backend/services/collection/imports/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
CollectionImportStudio,
CollectionImportWarnings,
)
from .collection_import_invalid_value_help_service import (
CollectionImportInvalidValueHelp,
CollectionImportInvalidValueHelpService,
)
from .collection_import_refusal_policy import (
CollectionImportRefusal,
CollectionImportRefusalPolicy,
Expand Down Expand Up @@ -77,6 +81,8 @@
"CollectionImportFailureNotificationService",
"CollectionImportField",
"CollectionImportGame",
"CollectionImportInvalidValueHelp",
"CollectionImportInvalidValueHelpService",
"CollectionImportPlatform",
"CollectionImportRefusal",
"CollectionImportRefusalAdminNotifier",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# ____ _ _ ____ _ _ _ _ ___
# / ___| | ___ _ _ __| |/ ___|___ | | | ___ ___| |_(_) ___ _ __ / _ \ _ __ _ __
# | | | |/ _ \| | | |/ _` | | / _ \| | |/ _ \/ __| __| |/ _ \| `_ \| | | | `_ \| `_ |
# | |___| | (_) | |_| | (_| | |__| (_) | | | __/ (__| |_| | (_) | | | | |_| | |_) | |_) |
# \____|_|\___/ \__,_|\__,_|\____\___|_|_|\___|\___|\__|_|\___/|_| |_|\___/| .__/| .__/
# |_| |_|
# Projet : CloudCollectionApp
# Date de creation : 2026-08-23
# Auteurs : OpenAI ChatGPT, Codex, Binda Sébastien
# Licence : Apache 2.0
#
# Description : aide a la correction des valeurs d'import refusees.

from dataclasses import dataclass

from .collection_private_information_contract import (
ALLOWED_REGIONS,
BOOLEAN_FALSE_LABELS,
BOOLEAN_TRUE_LABELS,
CONDITION_LABELS_BY_VALUE,
REGION_ALIASES_BY_VALUE,
)


@dataclass(frozen=True)
class CollectionImportInvalidValueHelp:
"""Decrit pourquoi une valeur importee a ete refusee.

Attributes:
field (str): Nom technique du champ refuse.
value (str): Valeur importee refusee.
reason (str): Explication lisible du refus.
possible_values (list[str]): Valeurs acceptees ou exemples utiles.
"""

field: str
value: str
reason: str
possible_values: list[str]

def to_dict(self) -> dict[str, object]:
"""Convertit l'aide en dictionnaire serialisable.

Args:
Aucun.

Returns:
dict[str, object]: Aide exploitable par l'IHM.
"""

return {
"field": self.field,
"value": self.value,
"reason": self.reason,
"possible_values": list(self.possible_values),
}


class CollectionImportInvalidValueHelpService:
"""Construit les aides de correction pour les valeurs d'import refusees."""

FIELD_HELPS = {
"release_date": {
"reason": "La date ne respecte pas un format reconnu ou est trop ancienne.",
"possible_values": ["1994", "1994-11-24", "24/11/1994"],
},
"buy_date": {
"reason": "La date d'achat ne respecte pas un format reconnu.",
"possible_values": ["2024-03-15", "15/03/2024"],
},
"purchase_price": {
"reason": "Le prix doit etre un nombre positif avec au plus deux decimales utiles.",
"possible_values": ["12", "12,50", "12.50"],
},
"grade": {
"reason": "La note doit etre numerique et compatible avec la base de notation choisie.",
"possible_values": ["8", "8/10", "82/100"],
},
"condition": {
"reason": "La valeur ne correspond pas a un etat physique reconnu.",
"possible_values": [],
},
"region": {
"reason": "La valeur ne correspond pas a une region ou version reconnue.",
"possible_values": [],
},
"has_manual": {
"reason": "La valeur ne correspond pas a une reponse oui/non reconnue.",
"possible_values": [],
},
"is_collector": {
"reason": "La valeur ne correspond pas a une reponse oui/non reconnue.",
"possible_values": [],
},
"has_steelbook": {
"reason": "La valeur ne correspond pas a une reponse oui/non reconnue.",
"possible_values": [],
},
"is_digital": {
"reason": "La valeur ne correspond pas a une reponse oui/non reconnue.",
"possible_values": [],
},
}

BOOLEAN_FIELDS = frozenset({"has_manual", "is_collector", "has_steelbook", "is_digital"})

def get_help(self, field: str, value: str) -> CollectionImportInvalidValueHelp:
"""Retourne l'aide de correction d'une valeur refusee.

Args:
field (str): Nom technique du champ refuse.
value (str): Valeur refusee telle qu'affichee dans le resume.

Returns:
CollectionImportInvalidValueHelp: Aide de correction.
"""

normalized_field = str(field or "").strip()
normalized_value = str(value or "").strip()
field_help = self.FIELD_HELPS.get(normalized_field)
if field_help is None:
return CollectionImportInvalidValueHelp(
normalized_field,
normalized_value,
"La valeur ne correspond pas au format attendu pour ce champ.",
[],
)
return CollectionImportInvalidValueHelp(
normalized_field,
normalized_value,
str(field_help["reason"]),
self._possible_values(normalized_field, field_help),
)

def _possible_values(self, field: str, field_help: dict[str, object]) -> list[str]:
if field == "region":
aliases = [
alias
for aliases_for_region in REGION_ALIASES_BY_VALUE.values()
for alias in aliases_for_region
]
return sorted(ALLOWED_REGIONS) + sorted(aliases)
if field == "condition":
values = [
label
for labels in CONDITION_LABELS_BY_VALUE.values()
for label in labels
]
return sorted(values)
if field in self.BOOLEAN_FIELDS:
return sorted(BOOLEAN_TRUE_LABELS) + sorted(BOOLEAN_FALSE_LABELS)
return list(field_help.get("possible_values") or [])
13 changes: 13 additions & 0 deletions backend/services/collection/imports/collection_import_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,24 @@ class CollectionImportWarnings:
invalid_wishlist (int): Nombre de lignes ignorees pour valeur wishlist invalide.
invalid_wishlist_values_found (list[str]): Valeurs wishlist invalides distinctes.
invalid_games (list[dict]): Jeux importes avec une information invalide ignoree.
skipped_mandatory_games (int): Lignes ignorees pour nom ou plateforme manquante.
platform_mappings (list[dict]): Synthese des plateformes lues et rattachees.
platform_matches (list[dict]): Plateformes rattachees avec verification manuelle.
skipped_games (list[dict]): Jeux ignores pendant l'import.
user_platform_matches (list[dict]): Messages simplifiés pour les plateformes a verifier.
user_skipped_games (list[dict]): Messages simplifiés pour les jeux ignores.
total_import_duration_seconds (float): Duree totale de l'import en secondes.
"""

invalid_wishlist: int = 0
invalid_wishlist_values_found: Optional[list[str]] = None
invalid_games: Optional[list[dict]] = None
skipped_mandatory_games: int = 0
platform_mappings: Optional[list[dict]] = None
platform_matches: Optional[list[dict]] = None
skipped_games: Optional[list[dict]] = None
user_platform_matches: Optional[list[dict]] = None
user_skipped_games: Optional[list[dict]] = None
total_import_duration_seconds: float = 0.0

def __post_init__(self):
Expand All @@ -115,6 +121,10 @@ def __post_init__(self):
object.__setattr__(self, "platform_matches", [])
if self.skipped_games is None:
object.__setattr__(self, "skipped_games", [])
if self.user_platform_matches is None:
object.__setattr__(self, "user_platform_matches", [])
if self.user_skipped_games is None:
object.__setattr__(self, "user_skipped_games", [])

def to_dict(self) -> dict[str, object]:
"""Convertit les warnings en dictionnaire serialisable.
Expand All @@ -130,9 +140,12 @@ def to_dict(self) -> dict[str, object]:
"invalid_wishlist": self.invalid_wishlist,
"invalid_wishlist_values_found": list(self.invalid_wishlist_values_found),
"invalid_games": list(self.invalid_games),
"skipped_mandatory_games": self.skipped_mandatory_games,
"platform_mappings": list(self.platform_mappings),
"platform_matches": list(self.platform_matches),
"skipped_games": list(self.skipped_games),
"user_platform_matches": list(self.user_platform_matches),
"user_skipped_games": list(self.user_skipped_games),
"total_import_duration_seconds": self.total_import_duration_seconds,
}

Expand Down
Loading