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 @@
{% if badge.icon %} - {% picture badge.icon img_alt=badge.name ratio="1/1" %} + {% svg_or_picture badge.icon img_alt=badge.name ratio="1/1" %} {% else %}
@@ -72,14 +71,11 @@

{{ badge.name }}

{# Niveau (pastille colorée) #} {# Level (colored pill) #}

+ {% if badge.category %} + {{ badge.category.icon }} {{ badge.category.name }} --- + {% endif %} {% if badge.level %} - {% if badge.level == 'beginner' %} - {{ badge.get_level_display }} - {% elif badge.level == 'intermediate' %} - {{ badge.get_level_display }} - {% elif badge.level == 'expert' %} - {{ badge.get_level_display }} - {% endif %} + {{ badge.level.name }} -- {% endif %} {% if badge.issuing_structure %} @@ -189,7 +185,7 @@

class="home-result-item">
{% if structure.logo %} - {% picture structure.logo img_alt=structure.name ratio="1/1" %} + {% svg_or_picture structure.logo img_alt=structure.name ratio="1/1" %} {% else %}
@@ -242,7 +238,7 @@

data-testid="badge-page-holder-{{ assignment.user.pk }}">
{% if assignment.user.avatar %} - {% picture assignment.user.avatar img_alt=assignment.user.get_full_name|default:assignment.user.username ratio="1/1" %} + {% svg_or_picture assignment.user.avatar img_alt=assignment.user.get_full_name|default:assignment.user.username ratio="1/1" %} {% else %}
diff --git a/templates/core/badge/partial/badge_create_form.html b/templates/core/badge/partial/badge_create_form.html index 9c33703..a9fadf4 100644 --- a/templates/core/badge/partial/badge_create_form.html +++ b/templates/core/badge/partial/badge_create_form.html @@ -1,81 +1,458 @@ -{# LOCALISATION : templates/core/badges/partial/create_form.html #} -{# Formulaire de creation de badge pour la modale HTMX (popup). #} -{# / Badge creation form for HTMX modal (popup). #} +{% load static badge_tags custom_tags %} {% load i18n %} -{% load custom_tags %} - -

{% translate "Forger un badge" %}

- -
- {% csrf_token %} - - {% if form.non_field_errors %} -
- {% for error in form.non_field_errors %} - {{ error }} - {% endfor %} -
- {% endif %} - - {# Icone / Icon #} -
- - {{ form.icon }} - {% if form.icon.errors %} -
{% for e in form.icon.errors %}{{ e }}{% endfor %}
- {% endif %} -
- {# Nom / Name #} -
- - {{ form.name }} - {% if form.name.errors %} -
{% for e in form.name.errors %}{{ e }}{% endfor %}
- {% endif %} -
+{% block extra_css %} + +{% endblock %} - {# Niveau / Level #} -
- - {{ form.level }} - {% if form.level.errors %} -
{% for e in form.level.errors %}{{ e }}{% endfor %}
- {% endif %} -
+{% block content %} - {# Structure emettrice / Issuing structure #} -
- - {{ form.issuing_structure }} - {% if form.issuing_structure.errors %} -
{% for e in form.issuing_structure.errors %}{{ e }}{% endfor %}
- {% endif %} -
+ + +
+ + + + O2Badge + + + +
+
+ +
+
+

{% translate "Générateur de Badges" %}

+

+ {% translate "Créez un badge numérique de qualité en choisissant une catégorie, un niveau et un titre." %} +

+
+
+ + + + + + +
+ + + + + + + + +

+ + {% translate "Icône" %} +

+

{% translate "Choisissez l'icône de votre badge." %}

+ +
+
+
+ + + + +
+
+ {% if errors.icon_type %} +

{{errors.icon_type.0}}

+ {% endif %} +
+
+ + + {% if errors.imported_icon %} +

{{errors.imported_icon.0}}

+ {% endif %} +
+ + + + +

+ + {% translate "Texte" %} +

+

{% translate "Donnez un titre à votre badge." %}

+ +
+ + +
+ {% if errors.title %} +

{{errors.title.0}}

+ {% endif %} + + +
+ + +
+ {% if errors.subtitle %} +

{{errors.subtitle.0}}

+ {% endif %} + + + +

+ + {% translate "Emetteur" %} +

+

{% translate "Qui crée ce badge ? Vous ou une structure ?" %}

+ +
+
+
+ + + + +
+
+ {% if errors.creator_type %} +

{{errors.creator_type.0}}

+ {% endif %} + +
+ +
+ + + {% if errors.structure_uuid %} +

{{errors.structure_uuid.0}}

+ {% endif %} +
+ + +

+ + {% translate "Catégorie" %} +

+

{% translate "Choisissez le type de badge." %}

+ +
+ {% for category in categories %} + + + {% endfor %} + {% if errors.category_uuid %} +

{{errors.category_uuid.0}}

+ {% endif %} + +
+ + +

+ + {% translate "Niveau" %} +

+

{% translate "Choisissez le niveau du badge." %}

+ +
+ {% for level in levels %} + + + {% endfor %} + {% if errors.level_uuid %} +

{{errors.level_uuid.0}}

+ {% endif %} + +
+ + +

+ + {% translate "Forme" %} +

+

{% translate "Choisissez la forme du badge." %}

+ +
+ {% for shape in shapes %} + + + {% endfor %} + {% if errors.shape %} +

{{errors.shape.0}}

+ {% endif %} +
+ + +

+ + {% translate "Critères" %} +

+

{% translate "Définissez les caractéristiques d'attribution du badge" %}

+ +
+ + + + {% if errors.criteria %} +

{{errors.criteria.0}}

+ {% endif %} +
+ + + +

+ + {% translate "Description" %} +

+

{% translate "Décrivez le badge" %}

+ +
+ + + + {% if errors.description %} +

{{errors.description.0}}

+ {% endif %} +
+ + + + +
+ + +
+ +
+ + +
+

{% translate "Prévisualisation" %}

+
+ +
+

+ {% translate "Choisissez une catégorie pour voir le badge." %} +

+
+
+
+ + - {# Boutons / Buttons #} -
- -
- + +{% endblock %} + +{% block extra_js %} + + +{% endblock %} diff --git a/templates/core/course/detail.html b/templates/core/course/detail.html index 71df40d..49d81d4 100644 --- a/templates/core/course/detail.html +++ b/templates/core/course/detail.html @@ -3,7 +3,6 @@ {# / Course/journey display page with Cytoscape graph. #} {% extends 'base/base.html' %} {% load i18n %} -{% load pictures %} {% load static %} {% load l10n %} {% load custom_tags %} @@ -216,7 +215,7 @@
{% if course.badge and course.badge.icon %} - {% picture course.badge.icon img_alt=course.badge.name img_class="parcours-badge-icon" ratio="1/1" %} + {% svg_or_picture course.badge.icon img_alt=course.badge.name img_class="parcours-badge-icon" ratio="1/1" %} {% else %}
@@ -275,7 +274,7 @@

data-testid="parcours-badge-{{ item.badge.pk }}">
{% if item.badge.icon %} - {% picture item.badge.icon img_alt=item.badge.name img_class="parcours-badge-icon" ratio="1/1" %} + {% svg_or_picture item.badge.icon img_alt=item.badge.name img_class="parcours-badge-icon" ratio="1/1" %} {% else %}
diff --git a/templates/core/course/edit.html b/templates/core/course/edit.html index b9dd12d..fc55a6d 100644 --- a/templates/core/course/edit.html +++ b/templates/core/course/edit.html @@ -3,7 +3,6 @@ {# / Course/journey display page with Cytoscape graph. #} {% extends 'base/base.html' %} {% load i18n %} -{% load pictures %} {% load static %} {% load l10n %} {% load custom_tags %} @@ -216,7 +215,7 @@
{% if course.badge and course.badge.icon %} - {% picture course.badge.icon img_alt=course.badge.name img_class="parcours-badge-icon" ratio="1/1" %} + {% svg_or_picture course.badge.icon img_alt=course.badge.name img_class="parcours-badge-icon" ratio="1/1" %} {% else %}
@@ -275,7 +274,7 @@

data-testid="parcours-badge-{{ item.badge.pk }}">
{% if item.badge.icon %} - {% picture item.badge.icon img_alt=item.badge.name img_class="parcours-badge-icon" ratio="1/1" %} + {% svg_or_picture item.badge.icon img_alt=item.badge.name img_class="parcours-badge-icon" ratio="1/1" %} {% else %}
diff --git a/templates/core/course/partial/badge_list_add.html b/templates/core/course/partial/badge_list_add.html index 2f37aa5..65d712f 100644 --- a/templates/core/course/partial/badge_list_add.html +++ b/templates/core/course/partial/badge_list_add.html @@ -1,4 +1,4 @@ -{% load pictures %} +{% load custom_tags %}