diff --git a/badge_generator/migrations/0004_auto_20260326_1143.py b/badge_generator/migrations/0004_auto_20260326_1143.py
new file mode 100644
index 0000000..848e4c1
--- /dev/null
+++ b/badge_generator/migrations/0004_auto_20260326_1143.py
@@ -0,0 +1,156 @@
+# Generated by Django 6.0.3 on 2026-03-26 10:43
+
+from django.db import migrations
+
+from badge_generator.illustrations import ILLUSTRATIONS_BY_ABBREVIATION
+
+
+def populate_badge_level_and_category(apps, schema_editor):
+ create_categories(apps, schema_editor)
+ create_levels(apps, schema_editor)
+
+def create_levels(apps, schema_editor):
+
+ BadgeLevel = apps.get_model('badge_generator', 'BadgeLevel')
+ levels_to_create = [
+ {
+ "name": "Découverte",
+ "description": "Premier contact avec le sujet.",
+ "rank": 1,
+ "posture_text": "JE DÉCOUVRE",
+ "stroke_width": 2,
+ },
+ {
+ "name": "Compréhension",
+ "description": "Je comprends les bases du sujet.",
+ "rank": 2,
+ "posture_text": "JE COMPRENDS",
+ "stroke_width": 4,
+ },
+ {
+ "name": "Pratique",
+ "description": "Je pratique de manière autonome.",
+ "rank": 3,
+ "posture_text": "JE PRATIQUE",
+ "stroke_width": 7,
+ },
+ {
+ "name": "Maîtrise",
+ "description": "Maîtrise avancée du sujet.",
+ "rank": 4,
+ "posture_text": "JE MAÎTRISE",
+ "stroke_width": 11,
+ },
+ {
+ "name": "Transmission",
+ "description": "Capacité à transmettre et former les autres.",
+ "rank": 5,
+ "posture_text": "JE TRANSMETS",
+ "stroke_width": 16,
+ },
+ ]
+
+ for level_data in levels_to_create:
+ BadgeLevel.objects.update_or_create(
+ name=level_data["name"],
+ defaults=level_data,
+ )
+
+
+def create_categories(apps, schema_editor):
+ BadgeCategory = apps.get_model('badge_generator', 'BadgeCategory')
+ categories_to_create = [
+ {
+ "name": "Compétence",
+ "abbreviation": "Cp",
+ "description": "Reconnaître un savoir ou une compétence acquise.",
+ "icon": "💡",
+ "color": "#0077B6",
+ "display_order": 1,
+ },
+ {
+ "name": "Savoir-faire",
+ "abbreviation": "Sf",
+ "description": "Reconnaître un savoir-faire technique ou manuel.",
+ "icon": "🔧",
+ "color": "#E76F51",
+ "display_order": 2,
+ },
+ {
+ "name": "Savoir-être",
+ "abbreviation": "Se",
+ "description": "Reconnaître des qualités humaines et relationnelles.",
+ "icon": "❤️",
+ "color": "#E63946",
+ "display_order": 3,
+ },
+ {
+ "name": "Savoir-vivre",
+ "abbreviation": "Sv",
+ "description": "Reconnaître le vivre-ensemble et le respect mutuel.",
+ "icon": "🤝",
+ "color": "#2A9D8F",
+ "display_order": 4,
+ },
+ {
+ "name": "Projet",
+ "abbreviation": "Pj",
+ "description": "Reconnaître l'initiative et le lancement de projets.",
+ "icon": "🚀",
+ "color": "#F4A261",
+ "display_order": 5,
+ },
+ {
+ "name": "Participation",
+ "abbreviation": "Pc",
+ "description": "Reconnaître la participation active et le volontariat.",
+ "icon": "🙌",
+ "color": "#50B83C",
+ "display_order": 6,
+ },
+ {
+ "name": "Groupe",
+ "abbreviation": "Gp",
+ "description": "Reconnaître le travail en équipe et la communauté.",
+ "icon": "👥",
+ "color": "#9C6ADE",
+ "display_order": 7,
+ },
+ {
+ "name": "Expérience",
+ "abbreviation": "Xp",
+ "description": "Reconnaître l'expérience acquise et les défis relevés.",
+ "icon": "🏔️",
+ "color": "#264653",
+ "display_order": 8,
+ },
+ ]
+
+ for category_data in categories_to_create:
+ # On recupere l'illustration SVG correspondante.
+ # Get the matching SVG illustration.
+ abbreviation = category_data["abbreviation"]
+ illustration_svg = ILLUSTRATIONS_BY_ABBREVIATION.get(abbreviation, "")
+
+ # On ajoute l'illustration et la couleur de texte par defaut.
+ # Add illustration and default text color.
+ category_data["illustration_svg"] = illustration_svg
+ category_data["text_color"] = "#473467"
+
+ # On utilise update_or_create pour ne pas creer de doublons.
+ # Use update_or_create to avoid duplicates.
+ BadgeCategory.objects.update_or_create(
+ name=category_data["name"],
+ defaults=category_data,
+ )
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('badge_generator', '0003_remove_generatedbadge_pictogram_and_more'),
+ ]
+
+ operations = [
+ migrations.RunPython(populate_badge_level_and_category),
+ ]
diff --git a/badge_generator/serializers.py b/badge_generator/serializers.py
index 4e0413b..3326f25 100644
--- a/badge_generator/serializers.py
+++ b/badge_generator/serializers.py
@@ -18,6 +18,7 @@
from badge_generator.models import BadgeCategory, BadgeLevel
from badge_generator.shapes import ALL_SHAPES, DEFAULT_SHAPE_KEY
+from core.models import Structure
# ============================================================================
@@ -183,3 +184,7 @@ def validate_shape(self, value):
if not value or value not in ALL_SHAPES:
return DEFAULT_SHAPE_KEY
return value
+
+
+
+
diff --git a/badge_generator/static/badge_generator/css/badge_generator.css b/badge_generator/static/badge_generator/css/badge_generator.css
index ab86a29..fac309a 100644
--- a/badge_generator/static/badge_generator/css/badge_generator.css
+++ b/badge_generator/static/badge_generator/css/badge_generator.css
@@ -513,3 +513,44 @@
max-height: 240px;
}
}
+
+#structures{
+ max-height: 0;
+ overflow: hidden;
+ animation: hideTest 0.5s forwards;
+}
+
+#structures.showed{
+ animation: showTest 0.5s forwards;
+ max-height: 100px;
+}
+
+
+#form_icon_import{
+ max-height: 0;
+ overflow: hidden;
+ animation: hideTest 0.5s forwards;
+}
+
+#form_icon_import.showed{
+ animation: showTest 0.5s forwards;
+ max-height: 100px;
+}
+
+@keyframes showTest {
+ 0%{
+ max-height: 0;
+ }
+ 100%{
+ max-height: 100px;
+ }
+}
+
+@keyframes hideTest {
+ 0%{
+ max-height: 100px;
+ }
+ 100%{
+ max-height: 0;
+ }
+}
\ No newline at end of file
diff --git a/badge_generator/static/badge_generator/js/badge_generator.js b/badge_generator/static/badge_generator/js/badge_generator.js
index 280d541..34e0f2b 100644
--- a/badge_generator/static/badge_generator/js/badge_generator.js
+++ b/badge_generator/static/badge_generator/js/badge_generator.js
@@ -15,274 +15,310 @@
* LOCALISATION : badge_generator/static/badge_generator/js/badge_generator.js
*/
-document.addEventListener("DOMContentLoaded", function () {
-
- // ================================================================
- // Variables pour stocker le minuteur de debounce.
- // On utilise un minuteur pour ne pas appeler la preview
- // a chaque frappe de clavier. On attend 300ms apres
- // la derniere frappe avant d'envoyer la requete.
- //
- // Debounce timer variable.
- // Waits 300ms after last keystroke before sending request.
- // ================================================================
-
- var debounce_timer_for_text_input = null;
- var DEBOUNCE_DELAY_IN_MILLISECONDS = 300;
-
-
- // ================================================================
- // Fonction : selectionner une carte dans une grille.
- // Quand on clique sur une carte, on la met en surbrillance.
- // On enleve la surbrillance des autres cartes du meme groupe.
- // On stocke la valeur dans le champ cache correspondant.
- // Gere les 3 types de cartes : categorie, niveau, forme.
- //
- // Select a card in a grid. Highlight it, unselect siblings,
- // store value in the matching hidden input.
- // Handles 3 card types: category, level, shape.
- // ================================================================
-
- function handle_card_selection(click_event) {
- var clicked_element = click_event.target.closest(".selection-card");
-
- // Si on n'a pas clique sur une carte, on ne fait rien.
- // If we didn't click a card, do nothing.
- if (!clicked_element) {
- return;
- }
-
- // On empeche le comportement par defaut du bouton.
- // Prevent default button behavior.
- click_event.preventDefault();
-
- // On cherche la grille parente pour deselectionner les autres cartes.
- // Find parent grid to deselect other cards.
- var parent_grid = clicked_element.closest(".selection-grid");
- if (parent_grid) {
- var all_cards_in_this_grid = parent_grid.querySelectorAll(".selection-card");
- for (var card_index = 0; card_index < all_cards_in_this_grid.length; card_index++) {
- all_cards_in_this_grid[card_index].classList.remove("selected");
- all_cards_in_this_grid[card_index].setAttribute("aria-checked", "false");
- }
- }
-
- // On ajoute la classe "selected" a la carte cliquee.
- // Add "selected" to the clicked card.
- clicked_element.classList.add("selected");
- clicked_element.setAttribute("aria-checked", "true");
- // On stocke la valeur dans le champ cache correspondant.
- // Les 3 types de cartes ont des attributs data differents.
- // Store value in the corresponding hidden input.
- // Each card type has a different data attribute.
+// ================================================================
+// Variables pour stocker le minuteur de debounce.
+// On utilise un minuteur pour ne pas appeler la preview
+// a chaque frappe de clavier. On attend 300ms apres
+// la derniere frappe avant d'envoyer la requete.
+//
+// Debounce timer variable.
+// Waits 300ms after last keystroke before sending request.
+// ================================================================
+
+var debounce_timer_for_text_input = null;
+var DEBOUNCE_DELAY_IN_MILLISECONDS = 300;
+
+
+// ================================================================
+// Fonction : selectionner une carte dans une grille.
+// Quand on clique sur une carte, on la met en surbrillance.
+// On enleve la surbrillance des autres cartes du meme groupe.
+// On stocke la valeur dans le champ cache correspondant.
+// Gere les 3 types de cartes : categorie, niveau, forme.
+//
+// Select a card in a grid. Highlight it, unselect siblings,
+// store value in the matching hidden input.
+// Handles 3 card types: category, level, shape.
+// ================================================================
+
+function handle_card_selection(click_event) {
+ var clicked_element = click_event.target.closest(".selection-card");
+
+ // Si on n'a pas clique sur une carte, on ne fait rien.
+ // If we didn't click a card, do nothing.
+ if (!clicked_element) {
+ return;
+ }
- if (clicked_element.classList.contains("category-card")) {
- var category_uuid = clicked_element.getAttribute("data-category-uuid");
- document.getElementById("selected-category-uuid").value = category_uuid;
+ // On empeche le comportement par defaut du bouton.
+ // Prevent default button behavior.
+ click_event.preventDefault();
+
+ // On cherche la grille parente pour deselectionner les autres cartes.
+ // Find parent grid to deselect other cards.
+ var parent_grid = clicked_element.closest(".selection-grid");
+ if (parent_grid) {
+ var all_cards_in_this_grid = parent_grid.querySelectorAll(".selection-card");
+ for (var card_index = 0; card_index < all_cards_in_this_grid.length; card_index++) {
+ all_cards_in_this_grid[card_index].classList.remove("selected");
+ all_cards_in_this_grid[card_index].setAttribute("aria-checked", "false");
}
+ }
- if (clicked_element.classList.contains("level-card")) {
- var level_uuid = clicked_element.getAttribute("data-level-uuid");
- document.getElementById("selected-level-uuid").value = level_uuid;
- }
+ // On ajoute la classe "selected" a la carte cliquee.
+ // Add "selected" to the clicked card.
+ clicked_element.classList.add("selected");
+ clicked_element.setAttribute("aria-checked", "true");
- if (clicked_element.classList.contains("shape-card")) {
- var shape_key = clicked_element.getAttribute("data-shape-key");
- document.getElementById("selected-shape").value = shape_key;
- }
+ // On stocke la valeur dans le champ cache correspondant.
+ // Les 3 types de cartes ont des attributs data differents.
+ // Store value in the corresponding hidden input.
+ // Each card type has a different data attribute.
- // On met a jour la preview et le bouton generer.
- // Update preview and generate button.
- request_badge_preview();
- update_generate_button_state();
+ if (clicked_element.classList.contains("category-card")) {
+ var category_uuid = clicked_element.getAttribute("data-category-uuid");
+ document.getElementById("selected-category-uuid").value = category_uuid;
}
+ if (clicked_element.classList.contains("level-card")) {
+ var level_uuid = clicked_element.getAttribute("data-level-uuid");
+ document.getElementById("selected-level-uuid").value = level_uuid;
+ }
- // ================================================================
- // Fonction : demander une previsualisation du badge.
- // On rassemble toutes les valeurs du formulaire
- // (categorie, niveau, forme, titre, sous-titre)
- // et on appelle le serveur pour generer le SVG.
- //
- // Request a badge preview.
- // Gather all form values and call the server to generate SVG.
- // ================================================================
-
- function request_badge_preview() {
- // On rassemble tous les parametres.
- // Gather all parameters.
- var category_uuid = document.getElementById("selected-category-uuid").value;
- var level_uuid = document.getElementById("selected-level-uuid").value;
- var shape_key = document.getElementById("selected-shape").value;
- var title = document.getElementById("badge-title").value;
- var subtitle = document.getElementById("badge-subtitle").value;
-
- // On construit l'URL de la preview avec les parametres.
- // Build preview URL with parameters.
- var preview_url = window.BADGE_PREVIEW_URL;
- var query_parameters = [];
-
- if (category_uuid) {
- query_parameters.push("category_uuid=" + encodeURIComponent(category_uuid));
- }
- if (level_uuid) {
- query_parameters.push("level_uuid=" + encodeURIComponent(level_uuid));
- }
- if (shape_key) {
- query_parameters.push("shape=" + encodeURIComponent(shape_key));
- }
- if (title) {
- query_parameters.push("title=" + encodeURIComponent(title));
- }
- if (subtitle) {
- query_parameters.push("subtitle=" + encodeURIComponent(subtitle));
- }
+ if (clicked_element.classList.contains("shape-card")) {
+ var shape_key = clicked_element.getAttribute("data-shape-key");
+ document.getElementById("selected-shape").value = shape_key;
+ }
- if (query_parameters.length > 0) {
- preview_url = preview_url + "?" + query_parameters.join("&");
- }
+ // On met a jour la preview et le bouton generer.
+ // Update preview and generate button.
+ request_badge_preview();
+ update_generate_button_state();
+}
+
+
+// ================================================================
+// Fonction : demander une previsualisation du badge.
+// On rassemble toutes les valeurs du formulaire
+// (categorie, niveau, forme, titre, sous-titre)
+// et on appelle le serveur pour generer le SVG.
+//
+// Request a badge preview.
+// Gather all form values and call the server to generate SVG.
+// ================================================================
+
+function request_badge_preview() {
+ var generate_icon = document.getElementById("icon_generate").checked
+ if (!generate_icon){
+ return;
+ }
- // On utilise HTMX pour charger la preview.
- // Use HTMX to load the preview.
- var preview_container = document.getElementById("badge-preview-container");
- if (preview_container && typeof htmx !== "undefined") {
- htmx.ajax("GET", preview_url, {
- target: "#badge-preview-container",
- swap: "innerHTML"
- });
- }
+ // On rassemble tous les parametres.
+ // Gather all parameters.
+ var category_uuid = document.getElementById("selected-category-uuid").value;
+ var level_uuid = document.getElementById("selected-level-uuid").value;
+ var shape_key = document.getElementById("selected-shape").value;
+ var title = document.getElementById("badge-title").value;
+ var subtitle = document.getElementById("badge-subtitle").value;
+
+ // On construit l'URL de la preview avec les parametres.
+ // Build preview URL with parameters.
+ var preview_url = window.BADGE_PREVIEW_URL;
+ var query_parameters = [];
+
+ if (category_uuid) {
+ query_parameters.push("category_uuid=" + encodeURIComponent(category_uuid));
+ }
+ if (level_uuid) {
+ query_parameters.push("level_uuid=" + encodeURIComponent(level_uuid));
+ }
+ if (shape_key) {
+ query_parameters.push("shape=" + encodeURIComponent(shape_key));
+ }
+ if (title) {
+ query_parameters.push("title=" + encodeURIComponent(title));
+ }
+ if (subtitle) {
+ query_parameters.push("subtitle=" + encodeURIComponent(subtitle));
}
+ if (query_parameters.length > 0) {
+ preview_url = preview_url + "?" + query_parameters.join("&");
+ }
- // ================================================================
- // Fonction : gerer la frappe dans les champs texte.
- // On attend 300ms apres la derniere frappe avant de demander
- // la previsualisation. C'est le "debounce".
- //
- // Handle text input with 300ms debounce before preview.
- // ================================================================
+ // On utilise HTMX pour charger la preview.
+ // Use HTMX to load the preview.
+ var preview_container = document.getElementById("badge-preview-container");
+ if (preview_container && typeof htmx !== "undefined") {
+ htmx.ajax("GET", preview_url, {
+ target: "#badge-preview-container",
+ swap: "innerHTML"
+ });
+ }
+}
+
+
+// ================================================================
+// Fonction : gerer la frappe dans les champs texte.
+// On attend 300ms apres la derniere frappe avant de demander
+// la previsualisation. C'est le "debounce".
+//
+// Handle text input with 300ms debounce before preview.
+// ================================================================
+
+function handle_text_input_with_debounce() {
+ // On annule le minuteur precedent si il existe.
+ // Cancel previous timer if it exists.
+ if (debounce_timer_for_text_input !== null) {
+ clearTimeout(debounce_timer_for_text_input);
+ }
- function handle_text_input_with_debounce() {
- // On annule le minuteur precedent si il existe.
- // Cancel previous timer if it exists.
- if (debounce_timer_for_text_input !== null) {
- clearTimeout(debounce_timer_for_text_input);
- }
+ // On demarre un nouveau minuteur.
+ // Start a new timer.
+ debounce_timer_for_text_input = setTimeout(function () {
+ request_badge_preview();
+ update_generate_button_state();
+ }, DEBOUNCE_DELAY_IN_MILLISECONDS);
+}
+
+
+// ================================================================
+// Fonction : mettre a jour l'etat du bouton "Generer".
+// Le bouton est desactive tant que la categorie, le niveau
+// et le titre ne sont pas remplis.
+//
+// Update generate button state.
+// Disabled until category, level and title are filled.
+// ================================================================
+
+function update_generate_button_state() {
+ var category_uuid = document.getElementById("selected-category-uuid").value;
+ var level_uuid = document.getElementById("selected-level-uuid").value;
+ var title = document.getElementById("badge-title").value.trim();
+ var generate_button = document.getElementById("generate-button");
- // On demarre un nouveau minuteur.
- // Start a new timer.
- debounce_timer_for_text_input = setTimeout(function () {
- request_badge_preview();
- update_generate_button_state();
- }, DEBOUNCE_DELAY_IN_MILLISECONDS);
+ if (!generate_button) {
+ return;
}
+ // On active le bouton seulement si les 3 champs sont remplis.
+ // Enable button only if all 3 fields are filled.
+ var all_fields_are_filled = (
+ category_uuid !== "" &&
+ level_uuid !== "" &&
+ title !== ""
+ );
- // ================================================================
- // Fonction : mettre a jour l'etat du bouton "Generer".
- // Le bouton est desactive tant que la categorie, le niveau
- // et le titre ne sont pas remplis.
- //
- // Update generate button state.
- // Disabled until category, level and title are filled.
- // ================================================================
+ generate_button.disabled = !all_fields_are_filled;
+}
- function update_generate_button_state() {
- var category_uuid = document.getElementById("selected-category-uuid").value;
- var level_uuid = document.getElementById("selected-level-uuid").value;
- var title = document.getElementById("badge-title").value.trim();
- var generate_button = document.getElementById("generate-button");
- if (!generate_button) {
- return;
- }
+// ================================================================
+// On attache les ecouteurs d'evenements.
+// Attach event listeners.
+// ================================================================
- // On active le bouton seulement si les 3 champs sont remplis.
- // Enable button only if all 3 fields are filled.
- var all_fields_are_filled = (
- category_uuid !== "" &&
- level_uuid !== "" &&
- title !== ""
- );
+// Clic sur les cartes de selection (delegation d'evenement sur le body).
+// Card click (event delegation on body).
+document.body.addEventListener("click", handle_card_selection);
- generate_button.disabled = !all_fields_are_filled;
- }
+// Frappe dans le titre.
+// Title input.
+var title_input = document.getElementById("badge-title");
+if (title_input) {
+ title_input.addEventListener("input", handle_text_input_with_debounce);
+}
+// Frappe dans le sous-titre.
+// Subtitle input.
+var subtitle_input = document.getElementById("badge-subtitle");
+if (subtitle_input) {
+ subtitle_input.addEventListener("input", handle_text_input_with_debounce);
+}
- // ================================================================
- // Fonction : generer le badge (appel POST final).
- // On envoie toutes les donnees au serveur pour sauvegarder.
- // Le serveur renvoie la page de resultat.
- //
- // Generate the badge (final POST call).
- // Send all data to server to save. Server returns result page.
- // ================================================================
-
- function generate_badge() {
- var category_uuid = document.getElementById("selected-category-uuid").value;
- var level_uuid = document.getElementById("selected-level-uuid").value;
- var shape_key = document.getElementById("selected-shape").value;
- var title = document.getElementById("badge-title").value;
- var subtitle = document.getElementById("badge-subtitle").value;
-
- // On verifie que tout est rempli.
- // Check that everything is filled.
- if (!category_uuid || !level_uuid || !title.trim()) {
- return;
- }
- // On envoie la requete POST avec HTMX.
- // Le resultat remplace le contenu des deux colonnes.
- // Send POST request with HTMX. Result replaces the two-column content.
- if (typeof htmx !== "undefined") {
- htmx.ajax("POST", window.BADGE_GENERATE_URL, {
- target: ".generator-two-columns",
- swap: "innerHTML",
- values: {
- "category_uuid": category_uuid,
- "level_uuid": level_uuid,
- "shape": shape_key,
- "title": title,
- "subtitle": subtitle
- },
- headers: {
- "X-CSRFToken": window.CSRF_TOKEN
- }
- });
- }
- }
+//
+// CSS classes for button style for the choices of creator
+var selected_classes = "btn btn-primary btn-lg";
+var not_selected_classes = "popup-cancel-btn";
- // ================================================================
- // On attache les ecouteurs d'evenements.
- // Attach event listeners.
- // ================================================================
- // Clic sur les cartes de selection (delegation d'evenement sur le body).
- // Card click (event delegation on body).
- document.body.addEventListener("click", handle_card_selection);
+function set_creator_type(event){
+ // Get the inputs radio label
+ var user_label = document.querySelector("label[for='as_user']");
+ var structure_label = document.querySelector("label[for='as_structure']");
- // Frappe dans le titre.
- // Title input.
- var title_input = document.getElementById("badge-title");
- if (title_input) {
- title_input.addEventListener("input", handle_text_input_with_debounce);
- }
+ // Get the div containing the structure select
+ var structure_div = document.querySelector("#structures");
- // Frappe dans le sous-titre.
- // Subtitle input.
- var subtitle_input = document.getElementById("badge-subtitle");
- if (subtitle_input) {
- subtitle_input.addEventListener("input", handle_text_input_with_debounce);
+ // Display the `structure_div` only if 'structure' input radio is selected
+ // And change the labels style accordingly
+ if(event.target.value === "user")
+ {
+ structure_div.classList.remove("showed")
+ user_label.className = selected_classes
+ structure_label.className = not_selected_classes
}
-
- // Clic sur le bouton "Generer".
- // Generate button click.
- var generate_button = document.getElementById("generate-button");
- if (generate_button) {
- generate_button.addEventListener("click", generate_badge);
+ else if(event.target.value === "structure")
+ {
+ structure_div.classList.add("showed")
+ user_label.className = not_selected_classes
+ structure_label.className = selected_classes
}
-
-
-});
+}
+
+var creator_type = document.querySelectorAll("input[name='creator_type']")
+creator_type.forEach((radio)=>{
+ radio.addEventListener("change",set_creator_type)
+})
+
+function set_import_type(event){
+ // Get the inputs radio label
+ var icon_import = document.querySelector("label[for='icon_import']");
+ var icon_generate = document.querySelector("label[for='icon_generate']");
+
+ // Get the div containing the structure select
+ var div_icon_import = document.querySelector("#form_icon_import");
+
+ // Display the `structure_div` only if 'structure' input radio is selected
+ // And change the labels style accordingly
+ if(event.target.value === "generate")
+ {
+ div_icon_import.classList.remove("showed")
+ icon_generate.className = selected_classes
+ icon_import.className = not_selected_classes
+ }
+ else if(event.target.value === "import")
+ {
+ div_icon_import.classList.add("showed")
+ icon_generate.className = not_selected_classes
+ icon_import.className = selected_classes
+ }
+}
+
+var icon_type = document.querySelectorAll("input[name='icon_type']")
+icon_type.forEach((radio)=>{
+ radio.addEventListener("change",set_import_type)
+})
+
+
+// Simple preview for uploaded logo
+document.getElementById('imported_icon').addEventListener('change', function (event) {
+ const file = event.target.files[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = function (e) {
+ let img = document.createElement("img")
+ img.src = e.target.result;
+ img.alt = "Icon preview";
+ img.style.maxWidth = "280px";
+
+ var img_container = document.getElementById('badge-preview-container');
+ img_container.innerHTML = ""
+ img_container.appendChild(img);
+ }
+ reader.readAsDataURL(file);
+ }
+});
\ No newline at end of file
diff --git a/badge_generator/templates/badge_generator/home.html b/badge_generator/templates/badge_generator/home.html
index da80078..7e7f019 100644
--- a/badge_generator/templates/badge_generator/home.html
+++ b/badge_generator/templates/badge_generator/home.html
@@ -1,4 +1,3 @@
-{% extends "base/base.html" %}
{% load static badge_tags %}
{% load i18n %}
diff --git a/badge_generator/views.py b/badge_generator/views.py
index 56b6668..5db8e16 100644
--- a/badge_generator/views.py
+++ b/badge_generator/views.py
@@ -14,7 +14,9 @@
import re
from django.http import HttpResponse
-from django.shortcuts import get_object_or_404, render
+from django.shortcuts import get_object_or_404, render, redirect
+from django.urls import reverse
+from django_htmx.http import HttpResponseClientRedirect
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny
@@ -23,6 +25,10 @@
from badge_generator.serializers import GenerateBadgeSerializer, PreviewBadgeSerializer
from badge_generator.shapes import ALL_SHAPES, DEFAULT_SHAPE_KEY
from badge_generator.svg_engine import generate_badge_svg
+from core.models import Badge, BadgeHistory, BadgeCriteria, Structure
+from django.core.files.base import ContentFile
+
+from core.validators import CreateBadgeValidator
class BadgeGeneratorViewSet(viewsets.ViewSet):
@@ -65,12 +71,15 @@ def list(self, request):
"path": shape_data["path"],
})
- return render(request, "badge_generator/home.html", {
+ structures = request.user.structures
+
+ return render(request, "badge_generator/badge_creation.html", {
"categories": all_categories,
"levels": all_levels,
"total_badges_generated": total_badges_generated,
"shapes": all_available_shapes,
"default_shape_key": DEFAULT_SHAPE_KEY,
+ "structures": structures,
})
# ========================================================================
@@ -233,4 +242,4 @@ def download_svg(self, request, pk=None):
response["Content-Disposition"] = (
f'attachment; filename="badge_{safe_filename}.svg"'
)
- return response
+ return response
\ No newline at end of file
diff --git a/core/forms.py b/core/forms.py
index b6bbb3b..e11596d 100644
--- a/core/forms.py
+++ b/core/forms.py
@@ -14,7 +14,7 @@ def __init__(self, *args, **kwargs):
class Meta:
model = Badge
- fields = ['name', 'icon', 'level', 'description', 'issuing_structure']
+ fields = ['name', 'icon', 'description', 'issuing_structure']
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control form-control-lg',
@@ -24,7 +24,7 @@ class Meta:
'class': 'form-control',
'accept': 'image/*'
}),
- 'level': forms.RadioSelect(),
+ # 'level': forms.RadioSelect(),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
@@ -35,57 +35,6 @@ class Meta:
}),
}
-class UserForm(forms.ModelForm):
- """
- Form for creating and updating users profiles
- """
- class Meta:
- model = User
- fields = ["first_name", "last_name", "email", "password", "avatar", "address"]
- widgets = {
- 'first_name': forms.TextInput(attrs={
- 'class': 'form-control form-control-lg',
- 'placeholder': 'Prénom'
- }),
- 'last_name': forms.TextInput(attrs={
- 'class': 'form-control form-control-lg',
- 'placeholder': 'Nom de famille'
- }),
- 'email': forms.TextInput(attrs={
- 'class': 'form-control form-control-lg',
- 'placeholder': 'Email'
- }),
- 'password': forms.PasswordInput(attrs={
- 'class': 'form-control form-control-lg',
- 'placeholder': 'Mot de passe'
- }),
- 'address': forms.TextInput(attrs={
- 'class': 'form-control form-control-lg',
- 'placeholder': 'Adresse ...'
- }),
- 'avatar': forms.FileInput(attrs={
- 'class': 'form-control',
- 'accept': 'image/*'
- }),
- }
-
- password_confirm = forms.CharField(widget=forms.PasswordInput(
- attrs={
- 'class': 'form-control form-control-lg',
- 'placeholder': 'Confirmation du mot de passe'
- }), label='Confirmation du mot de passe')
-
- def clean(self):
- """Check if password and password_confirm are matching"""
- cleaned_data = super().clean()
- password = cleaned_data.get('password')
- password_confirm = cleaned_data.get('password_confirm')
- if password != password_confirm:
- self.add_error(None, 'Les mots de passes ne correspondent pas')
-
- return cleaned_data
-
-
class PartialUserForm(forms.ModelForm):
"""
Forms for editing user profiles
diff --git a/core/migrations/0004_badge_new_level_badge_type.py b/core/migrations/0004_badge_new_level_badge_type.py
new file mode 100644
index 0000000..5065cc8
--- /dev/null
+++ b/core/migrations/0004_badge_new_level_badge_type.py
@@ -0,0 +1,25 @@
+# Generated by Django 6.0.3 on 2026-03-26 10:36
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('badge_generator', '0003_remove_generatedbadge_pictogram_and_more'),
+ ('core', '0003_remove_structure_latitude_remove_structure_longitude'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='badge',
+ name='new_level',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='badges', to='badge_generator.badgelevel', verbose_name='Niveau'),
+ ),
+ migrations.AddField(
+ model_name='badge',
+ name='type',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='badges', to='badge_generator.badgecategory', verbose_name='Type'),
+ ),
+ ]
diff --git a/core/migrations/0005_auto_20260326_1136.py b/core/migrations/0005_auto_20260326_1136.py
new file mode 100644
index 0000000..6643346
--- /dev/null
+++ b/core/migrations/0005_auto_20260326_1136.py
@@ -0,0 +1,35 @@
+# Generated by Django 6.0.3 on 2026-03-26 10:36
+
+from django.db import migrations
+
+def migrate_level_to_level(apps, schema_editor):
+ Badge = apps.get_model('core', 'Badge')
+ BadgeLevel = apps.get_model('badge_generator', 'BadgeLevel')
+
+ # Get the levels, their names are populated in the 0004 from badge_generator
+ beginner = BadgeLevel.objects.get(name="Découverte")
+ intermediate = BadgeLevel.objects.get(name="Pratique")
+ expert = BadgeLevel.objects.get(name="Maîtrise")
+
+ for badge in Badge.objects.all():
+ if badge.level is not None:
+ if badge.level == "beginner":
+ badge.new_level = beginner
+ elif badge.level == "intermediate":
+ badge.new_level = intermediate
+ elif badge.level == "expert":
+ badge.new_level = expert
+ badge.save()
+
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0004_badge_new_level_badge_type'),
+ ('badge_generator', '0004_auto_20260326_1143'),
+ ]
+
+ operations = [
+ migrations.RunPython(migrate_level_to_level),
+ ]
diff --git a/core/migrations/0006_remove_badge_level.py b/core/migrations/0006_remove_badge_level.py
new file mode 100644
index 0000000..c4301e4
--- /dev/null
+++ b/core/migrations/0006_remove_badge_level.py
@@ -0,0 +1,17 @@
+# Generated by Django 6.0.3 on 2026-03-26 11:39
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0005_auto_20260326_1136'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='badge',
+ name='level',
+ ),
+ ]
diff --git a/core/migrations/0007_rename_new_level_badge_level.py b/core/migrations/0007_rename_new_level_badge_level.py
new file mode 100644
index 0000000..7b0df8a
--- /dev/null
+++ b/core/migrations/0007_rename_new_level_badge_level.py
@@ -0,0 +1,18 @@
+# Generated by Django 6.0.3 on 2026-03-26 11:39
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0006_remove_badge_level'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='badge',
+ old_name='new_level',
+ new_name='level',
+ ),
+ ]
diff --git a/core/migrations/0008_rename_type_badge_category.py b/core/migrations/0008_rename_type_badge_category.py
new file mode 100644
index 0000000..80ac114
--- /dev/null
+++ b/core/migrations/0008_rename_type_badge_category.py
@@ -0,0 +1,18 @@
+# Generated by Django 6.0.3 on 2026-03-26 11:42
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0007_rename_new_level_badge_level'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='badge',
+ old_name='type',
+ new_name='category',
+ ),
+ ]
diff --git a/core/models.py b/core/models.py
index 3bf34f5..365e5a3 100644
--- a/core/models.py
+++ b/core/models.py
@@ -4,6 +4,7 @@
from django.utils import timezone
from pictures.models import PictureField
from django.db.models import Q, CheckConstraint
+from badge_generator.models import BadgeLevel, BadgeCategory
# Create your models here.
@@ -301,12 +302,14 @@ class Badge(models.Model):
('expert', 'Expert'),
]
+ level = models.ForeignKey(BadgeLevel, on_delete=models.SET_NULL, related_name='badges', verbose_name="Niveau",null=True, blank=True)
+ category = models.ForeignKey(BadgeCategory, on_delete=models.SET_NULL, related_name='badges', verbose_name="Type",null=True, blank=True)
+
name = models.CharField(max_length=100, verbose_name="Nom")
icon_width = models.PositiveIntegerField(blank=True, null=True, editable=False)
icon_height = models.PositiveIntegerField(blank=True, null=True, editable=False)
icon = PictureField(upload_to='badges/icons/', blank=True, null=True, verbose_name="Icône",
aspect_ratios=[None, "1/1"], width_field='icon_width', height_field='icon_height')
- level = models.CharField(max_length=20, choices=LEVEL_CHOICES, verbose_name="Niveau",null=True, blank=True)
description = models.TextField(verbose_name="Description")
is_dream_badge = models.BooleanField(default=False, verbose_name="Badge de rêve")
@@ -329,8 +332,8 @@ class Meta:
verbose_name_plural = "Badges"
ordering = ['name']
- def __str__(self):
- return f"{self.name} ({self.get_level_display()})"
+ # def __str__(self):
+ # return f"{self.name} ({self.get_level_display()})"
@property
def valid_structures(self):
@@ -644,6 +647,8 @@ def add_to_course(course, badge, parent):
if parent:
item = CourseItem.objects.get(badge=parent, course=course)
c.parents.add(item)
+
+
class BadgeCriteria(models.Model):
"""
Critères d'attribution d'un badge par une structure.
diff --git a/core/templatetags/custom_tags.py b/core/templatetags/custom_tags.py
index cc8a112..6e157e2 100644
--- a/core/templatetags/custom_tags.py
+++ b/core/templatetags/custom_tags.py
@@ -1,38 +1,72 @@
from django import template
from django.template import loader
+from pictures.templatetags.pictures import picture
+from django.utils.html import format_html
register = template.Library()
POPUP_CLOSE_JS_FUNCTION = "closePopup"
POPUP_OPEN_JS_FUNCTION = "openPopup"
+POPUP_SCROLL_TOP_JS_FUNCTION="scrollToTopContent"
POPUP_CONTENT_ID = "customPopup-content"
+
@register.simple_block_tag
def popup(content, popup_width="40%", *args, **kwargs):
outside_click_close = kwargs.pop("outside_click_close", False)
esc_key_close = kwargs.pop("esc_key_close", True)
+ template = loader.get_template("base/includes/popup.html")
- template = loader.get_template('base/includes/popup.html')
+ return template.render(
+ {
+ "content": content,
+ "open_func_name": POPUP_OPEN_JS_FUNCTION,
+ "close_func_name": POPUP_CLOSE_JS_FUNCTION,
+ "scroll_top_func_name":POPUP_SCROLL_TOP_JS_FUNCTION,
+ "popup_content_id": POPUP_CONTENT_ID,
+ "popup_width": popup_width,
+ "outside_click_close": outside_click_close,
+ "esc_key_close": esc_key_close,
+ }
+ )
- return template.render({
- "content": content,
- "open_func_name": POPUP_OPEN_JS_FUNCTION,
- "close_func_name": POPUP_CLOSE_JS_FUNCTION,
- "popup_content_id":POPUP_CONTENT_ID,
- "popup_width":popup_width,
- "outside_click_close":outside_click_close,
- "esc_key_close" : esc_key_close
- })
@register.simple_tag
def popup_close():
return f"{POPUP_CLOSE_JS_FUNCTION}()"
+
@register.simple_tag
def popup_open():
return f"{POPUP_OPEN_JS_FUNCTION}()"
+
@register.simple_tag
def popup_content_id():
- return f"#{POPUP_CONTENT_ID}"
\ No newline at end of file
+ return f"#{POPUP_CONTENT_ID}"
+
+@register.simple_tag
+def popup_scroll_to_top():
+ return f"{POPUP_SCROLL_TOP_JS_FUNCTION}()"
+
+
+def is_svg(file_field):
+ """Check if the file is an SVG."""
+ if not file_field:
+ return False
+ name = getattr(file_field, "name", str(file_field))
+ return name.lower().endswith(".svg")
+
+@register.simple_tag()
+def svg_or_picture(img_src, img_alt, ratio, **kwargs):
+ """
+ If the file is a svg, return an html img
+ Else return the image using "picture" template tag from django-pictures
+
+ The usage is the same to the "picture" template tag from django-pictures
+ """
+ if is_svg(img_src.url):
+ return format_html("
", img_src.url, img_alt)
+
+ return picture(img_src, img_alt=img_alt, ratio=ratio, **kwargs)
\ No newline at end of file
diff --git a/core/urls.py b/core/urls.py
index fe4ea4b..bc49f42 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -1,6 +1,7 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
+import badge_generator.views
app_name = 'core'
@@ -19,8 +20,8 @@
urlpatterns = [
# Creation routes
path('badge/create/', views.BadgeViewSet.as_view({'get': 'create_badge', 'post': 'create_badge'}), name='create_badge'),
+ # path('badge/create/', views.BadgeViewSet.as_view({'get': 'create_badge', 'post': 'create_badge'}), name='create_badge'),
path('structure/create/', views.StructureViewSet.as_view({'get': 'create_association', 'post': 'create_association'}), name='create_association'),
- path('user/create/', views.UserViewSet.as_view({'get': 'create_user', 'post': 'create_user'}), name='create_user'),
# Edition routes
path('users//edit', views.UserViewSet.as_view({'get': 'edit', 'post': 'edit'}), name='edit-profile'),
diff --git a/core/validators.py b/core/validators.py
index 08f7996..4366727 100644
--- a/core/validators.py
+++ b/core/validators.py
@@ -1,9 +1,12 @@
from django.shortcuts import get_object_or_404
from rest_framework import serializers
+from badge_generator.models import BadgeLevel, BadgeCategory
+from badge_generator.shapes import DEFAULT_SHAPE_KEY, ALL_SHAPES
from core.admin import StructureAdmin
from core.models import Badge, User, Structure
from mapview.models import Marker
+from django.utils.translation import gettext_lazy as _
class BadgeAssignmentValidator(serializers.Serializer):
@@ -201,3 +204,135 @@ def update(self, instance, validated_data):
# Marker info
latitude = serializers.FloatField(required=False)
longitude = serializers.FloatField(required=False)
+
+class CreateBadgeValidator(serializers.Serializer):
+ """
+ Validator for creating a badge and an associated icon
+ The request context MUST be populated.
+ """
+
+ category_uuid = serializers.UUIDField(
+ error_messages={
+ "required": _("Veuillez choisir une catégorie."),
+ "invalid": _("Cette catégorie n'est pas valide."),
+ }
+ )
+
+ level_uuid = serializers.UUIDField(
+ error_messages={
+ "required": _("Veuillez choisir un niveau."),
+ "invalid": _("Ce niveau n'est pas valide."),
+ }
+ )
+
+ title = serializers.CharField(
+ max_length=100,
+ error_messages={
+ "required": _("Le titre est obligatoire."),
+ "max_length": _("Le titre est trop long (100 caractères max)."),
+ "blank": _("Le titre ne peut pas être vide."),
+ }
+ )
+
+ subtitle = serializers.CharField(
+ max_length=100,
+ required=False,
+ allow_blank=True,
+ default="",
+ )
+
+ # La forme choisie par l'utilisateur. Par defaut "starburst".
+ # Shape chosen by the user. Default is "starburst".
+ shape = serializers.CharField(
+ max_length=30,
+ required=False,
+ allow_blank=True,
+ default=DEFAULT_SHAPE_KEY,
+ )
+
+ description = serializers.CharField()
+
+ criteria = serializers.CharField()
+
+ # Icon related
+ icon_type = serializers.CharField(
+ error_messages={
+ "blank" : _("Veuillez choisir une option"),
+ "invalid": _("Cette option n'est pas valide."),
+ }
+ )
+
+ imported_icon = serializers.ImageField(required=False)
+
+ # Badge creator related
+ creator_type = serializers.CharField(
+ error_messages={
+ "blank" : _("Veuillez choisir une option"),
+ "invalid": _("Cette option n'est pas valide."),
+ }
+ )
+
+ structure_uuid = serializers.CharField(
+ error_messages={
+ "required": _("Veuillez choisir une structure."),
+ "invalid": _("Cette structure n'est pas valide."),
+ },
+ required=False,
+ )
+
+ def validate_category_uuid(self, value):
+ # On verifie que la categorie existe.
+ # Check that the category exists.
+ category_exists = BadgeCategory.objects.filter(uuid=value).exists()
+ if not category_exists:
+ raise serializers.ValidationError(
+ _("Cette catégorie n'existe pas.")
+ )
+ return value
+
+ def validate_level_uuid(self, value):
+ # On verifie que le niveau existe.
+ # Check that the level exists.
+ level_exists = BadgeLevel.objects.filter(uuid=value).exists()
+ if not level_exists:
+ raise serializers.ValidationError(
+ _("Ce niveau n'existe pas.")
+ )
+ return value
+
+ def validate_icon_type(self,value):
+ # Check if the icon type is valide
+ if value == "import" or value == "generate":
+ return value
+
+ return serializers.ValidationError(_("Vous devez choisir un type d'icône."))
+
+ def validate_creator_type(self,value):
+ # Check if the creator type is valide
+ if value == "structure" or value == "user":
+ return value
+
+ raise serializers.ValidationError(_("Vous devez choisir qui émet ce badge."))
+
+ def validate_structure_uuid(self, value):
+ # Check if we create the badge as a structure. If so, check if the structure is valid and the user is editor
+ if self.initial_data.get("creator_type",None) == "structure":
+ structure = Structure.objects.filter(uuid=value)
+ structure_exist = structure.exists()
+ if not structure_exist:
+ raise serializers.ValidationError("Veuillez choisir une structure valide.")
+
+ # Get the user from the given context
+ user = self.context["request"].user
+
+ if not structure[0].is_editor(user) and not structure[0].is_admin(user):
+ raise serializers.ValidationError(_("Vous n'êtes pas éditeur de cette structure."))
+
+ return value
+
+ def validate_shape(self, value):
+ # Si la forme est vide ou inconnue, on prend la forme par defaut.
+ # If shape is empty or unknown, use default.
+ if not value or value not in ALL_SHAPES:
+ raise serializers.ValidationError(_("Veuillez choisir une forme."))
+ return value
diff --git a/core/views.py b/core/views.py
index f47787c..42177ab 100644
--- a/core/views.py
+++ b/core/views.py
@@ -2,6 +2,7 @@
from django.contrib import messages
from django.contrib.auth import logout, get_user_model, authenticate, login
from django.core.exceptions import ValidationError, PermissionDenied
+from django.core.files.base import ContentFile
from django.core.signing import SignatureExpired
from django.core.validators import validate_email
from django.http import HttpResponse, JsonResponse
@@ -12,15 +13,18 @@
from rest_framework import viewsets
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated, AllowAny
-from rest_framework.decorators import action,authentication_classes, permission_classes
+from rest_framework.decorators import action
+from badge_generator.models import BadgeCategory, BadgeLevel
+from badge_generator.shapes import ALL_SHAPES, DEFAULT_SHAPE_KEY
+from badge_generator.svg_engine import generate_badge_svg
from .helpers import TokenHelper
from .helpers.utils import get_or_create_user, invite_user_to_structure
from .models import Structure, Badge, User, BadgeAssignment, BadgeEndorsement, BadgeHistory, BadgeCriteria, Course, CourseItem
-from .forms import BadgeForm, UserForm, PartialUserForm
+from .forms import BadgeForm, PartialUserForm
from .permissions import IsBadgeEditor, IsStructureAdmin, CanEditUser, CanAssignBadge, CanEndorseBadge, CanEditCourse
from .validators import BadgeAssignmentValidator, BadgeEndorsementValidator, DreamBadgeValidator, InviteUserValidator, \
- CreateCourseValidator, BadgeSelfAssignmentValidator, CreateStructureValidator
+ CreateCourseValidator, BadgeSelfAssignmentValidator, CreateStructureValidator, CreateBadgeValidator
def raise403(request, msg=None):
@@ -1053,59 +1057,132 @@ def delete(self, request, pk=None):
@action(detail=False, methods=['get', 'post'])
def create_badge(self, request):
"""
- Cree un nouveau badge.
- Si requete HTMX, retourne le formulaire en partiel pour la modale.
- / Create a new badge. Returns partial for HTMX modal.
-
- LOCALISATION : core/views.py
+ Create a new badge and an icon for it.
"""
+ if not request.htmx:
+ return raise403(request)
- # Pre-remplir le formulaire depuis les query params (recherche ou page lieu)
- # / Pre-fill form from query params (search or structure page)
- default_structure = request.GET.get('structure', '')
- default_name = request.GET.get('name', '')
+ # Get all categories and levels
+ all_categories = BadgeCategory.objects.all()
+ all_levels = BadgeLevel.objects.all()
+
+ # On prepare la liste des formes disponibles pour le template.
+ # Build shape list for the template.
+ all_available_shapes = []
+ for shape_key, shape_data in ALL_SHAPES.items():
+ all_available_shapes.append({
+ "key": shape_key,
+ "name": shape_data["name"],
+ "description": shape_data["description"],
+ "path": shape_data["path"],
+ })
- if request.method == 'POST':
- form = BadgeForm(request.POST, request.FILES, request=request)
- if form.is_valid():
- badge = form.save()
- # Cree une entree historique pour la creation du badge
- # / Create a history entry for badge creation
- BadgeHistory.objects.create(
- badge=badge,
- action="creation",
- details="Badge crée"
- )
- # Redirige vers la page badge (HTMX ou classique)
- # / Redirect to badge page (HTMX or classic)
- messages.success(request,"Badge ajouté avec succès ! ")
- if request.htmx:
- return HttpResponseClientRedirect(
- reverse('core:home-badge-detail', kwargs={'badge_pk': badge.pk})
- )
- return redirect(reverse('core:home-badge-detail', kwargs={'badge_pk': badge.pk}))
- else:
- initial_data = {}
- if default_structure:
- initial_data['issuing_structure'] = default_structure
- if default_name:
- initial_data['name'] = default_name
- form = BadgeForm(initial=initial_data, request=request)
-
- # Toutes les structures pour le dropdown / All structures for dropdown
- structures = Structure.objects.all()
+ structures = request.user.structures
- # Partiel HTMX pour la modale / HTMX partial for modal
- if request.htmx:
- return render(request, 'core/badge/partial/badge_create_form.html', {
- 'form': form,
+ # Prepare the context
+ context = {
+ "categories": all_categories,
+ "levels": all_levels,
+ "shapes": all_available_shapes,
+ "default_shape_key": DEFAULT_SHAPE_KEY,
+ "structures": structures,
+ }
+
+ # return the template if the method is GET
+ if request.method == "GET":
+ return render(request, "core/badge/partial/badge_create_form.html", context=context)
+
+
+ # Validate the data received from the POST request
+ validator = CreateBadgeValidator(data=request.data,context={"request":request})
+ is_valid = validator.is_valid()
+
+ if not is_valid:
+ # Update the context with errors and defaults values
+ context.update({
+ "errors" : validator.errors,
+ "defaults" : validator.data,
+ "default_shape_key":validator.data["shape"]
})
+ return render(request, "core/badge/partial/badge_create_form.html", context=context)
- return render(request, 'core/badges/create.html', {
- 'title': 'openbadge.coop - Forger un Badge',
- 'structures': structures,
- 'form': form
- })
+ validated = validator.validated_data
+
+ # On cherche les objets dans la base de donnees.
+ # Find database objects.
+ chosen_category = get_object_or_404(
+ BadgeCategory, uuid=validated["category_uuid"]
+ )
+ chosen_level = get_object_or_404(
+ BadgeLevel, uuid=validated["level_uuid"]
+ )
+
+ # Create a dict with the badge data
+ badge_data = {
+ "name":validated["title"],
+ "description":validated["description"],
+ "category":chosen_category,
+ "level":chosen_level,
+ }
+
+ # Check the creator type and populate the badge_data accordingly
+ if validated["creator_type"] == "structure":
+ structure = Structure.objects.get(uuid=validated["structure_uuid"])
+ badge_data["issuing_structure"] = structure
+ else:
+ badge_data["user"] = request.user
+
+ # Create the badge
+ badge = Badge(
+ **badge_data
+ )
+
+ if validated["icon_type"] == "generate":
+ # On recupere la forme choisie.
+ # Get the chosen shape.
+ shape_key = validated.get("shape", DEFAULT_SHAPE_KEY)
+
+ # On genere le SVG final avec la forme choisie.
+ # Generate final SVG with chosen shape.
+ svg_text = generate_badge_svg(
+ category_name=chosen_category.name,
+ category_color=chosen_category.color,
+ level_stroke_width=chosen_level.stroke_width,
+ level_posture_text=chosen_level.posture_text,
+ illustration_svg=chosen_category.illustration_svg,
+ title=validated["title"],
+ subtitle=validated.get("subtitle", ""),
+ shape_key=shape_key,
+ )
+
+ # Saving the svg file like that make an exception but save it anyway
+ try:
+ svg_file = ContentFile(svg_text.encode("utf-8"))
+ badge.icon.save("icon.svg", svg_file)
+ except TypeError:
+ print("error svg file")
+
+ # Save the badge to db
+ elif validated["icon_type"] == "import" and validated["imported_icon"]:
+ badge.icon = validated["imported_icon"]
+
+ badge.save()
+
+ # Create a BadgeHistory
+ badgeHistory = BadgeHistory(
+ badge=badge,
+ action="creation",
+ details="Badge crée"
+ )
+
+ if validated["creator_type"] == "structure":
+ # Create a BadgeCriteria
+ badgeCriteria = BadgeCriteria(
+ badge=badge,
+ structure=structure,
+ criteria=validated["criteria"],
+ )
+ return redirect_reload(reverse('core:home-badge-detail', kwargs={'badge_pk': badge.pk}))
@action(detail=True, methods=["get", "post"])
def endorse(self, request, pk=None):
@@ -1713,29 +1790,6 @@ def retrieve(self, request, pk=None):
'structures': structures
})
- @action(detail=False, methods=['get', 'post'])
- def create_user(self, request):
- """
- Create a new user.
- """
- return raise403(request)
- if request.method == 'POST':
- form = UserForm(request.POST, request.FILES)
- if form.is_valid():
- user = form.save(commit=False)
- user.username = f"{form.cleaned_data['first_name']}.{form.cleaned_data['last_name']}".lower()
- user.set_password(form.cleaned_data['password'])
- user.save()
-
- return redirect(reverse('core:user-detail', kwargs={'pk': user.pk}))
- else:
- form = UserForm()
-
- return render(request, 'core/users/create.html', {
- 'title': 'openbadge.coop - Créer un utilisateur',
- 'form': form,
- })
-
@action(detail=True, methods=['get', 'post'],name="edit-profile")
def edit(self,request,pk=None):
"""
diff --git a/static/css/custom.css b/static/css/custom.css
index 3de8828..fc21cda 100644
--- a/static/css/custom.css
+++ b/static/css/custom.css
@@ -2632,4 +2632,9 @@ picture.text-center img.badge-icon {
justify-content: center;
padding: 0.85rem 1.5rem;
}
+}
+
+.badge-svg-icon{
+ width: 100%;
+ height: 100%;
}
\ No newline at end of file
diff --git a/templates/base/includes/popup.html b/templates/base/includes/popup.html
index 94dcc14..a32d8a3 100644
--- a/templates/base/includes/popup.html
+++ b/templates/base/includes/popup.html
@@ -220,6 +220,10 @@
popup.classList.remove("showed")
}
+ function {{ scroll_top_func_name }}(){
+ document.querySelector('#{{ popup_content_id }}').scrollTo({top: 0, behavior: 'smooth'})
+ }
+
{% if outside_click_close %}
document.querySelector("#customPopup-bg").addEventListener("click",{{ close_func_name }})
{% endif %}
diff --git a/templates/core/badge/index.html b/templates/core/badge/index.html
index b027909..4e70fd3 100644
--- a/templates/core/badge/index.html
+++ b/templates/core/badge/index.html
@@ -3,7 +3,6 @@
{# / Dedicated badge page. Shows header, structures, holders, actions, map. #}
{% extends 'base/base.html' %}
{% load i18n %}
-{% load pictures %}
{% load static %}
{% load l10n %}
{% load custom_tags %}
@@ -51,7 +50,7 @@