Spaces:
Sleeping
Sleeping
Guardado de progreso con nappging
Browse files- tecnicas/admin.py +6 -1
- tecnicas/controllers/api_controller/rating_napping_controller.py +86 -2
- tecnicas/controllers/views_controller/sessions_tester/tests_forms/test_napping_controller.py +24 -1
- tecnicas/static/js/test-napping.js +26 -2
- tecnicas/templates/tecnicas/forms_tester/test_napping.html +47 -2
- tecnicas/views/apis/rating_napping.py +1 -1
tecnicas/admin.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
from django.contrib import admin
|
| 2 |
|
| 3 |
-
from .models import CategoriaTecnica, TipoTecnica, TipoEscala, EstiloPalabra, Catador, Presentador, Tecnica, SesionSensorial, EsAtributo, Palabra, Vocabulario, Etiqueta, Escala, EtiquetasEscala, Producto, Participacion, Orden, Posicion, Dato, ValorDecimal, ValorBooleano, Calificacion, ListaPalabras, GrupoProducto, Modalidad
|
| 4 |
|
| 5 |
# Register your models here.
|
| 6 |
admin.site.register(CategoriaTecnica)
|
|
@@ -32,6 +32,11 @@ admin.site.register(Dato)
|
|
| 32 |
admin.site.register(ValorDecimal)
|
| 33 |
admin.site.register(ValorBooleano)
|
| 34 |
admin.site.register(Calificacion)
|
|
|
|
| 35 |
admin.site.register(ListaPalabras)
|
| 36 |
admin.site.register(GrupoProducto)
|
|
|
|
| 37 |
admin.site.register(Modalidad)
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from django.contrib import admin
|
| 2 |
|
| 3 |
+
from .models import CategoriaTecnica, TipoTecnica, TipoEscala, EstiloPalabra, Catador, Presentador, Tecnica, SesionSensorial, EsAtributo, Palabra, Vocabulario, Etiqueta, Escala, EtiquetasEscala, Producto, Participacion, Orden, Posicion, Dato, ValorDecimal, ValorBooleano, Calificacion, ListaPalabras, GrupoProducto, Modalidad, TecnicaModalidad, DatoPunto
|
| 4 |
|
| 5 |
# Register your models here.
|
| 6 |
admin.site.register(CategoriaTecnica)
|
|
|
|
| 32 |
admin.site.register(ValorDecimal)
|
| 33 |
admin.site.register(ValorBooleano)
|
| 34 |
admin.site.register(Calificacion)
|
| 35 |
+
|
| 36 |
admin.site.register(ListaPalabras)
|
| 37 |
admin.site.register(GrupoProducto)
|
| 38 |
+
|
| 39 |
admin.site.register(Modalidad)
|
| 40 |
+
admin.site.register(TecnicaModalidad)
|
| 41 |
+
|
| 42 |
+
admin.site.register(DatoPunto)
|
tecnicas/controllers/api_controller/rating_napping_controller.py
CHANGED
|
@@ -1,12 +1,96 @@
|
|
| 1 |
from django.http import JsonResponse
|
| 2 |
from django.http import HttpRequest
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
class RatingNappingController:
|
| 5 |
@staticmethod
|
| 6 |
-
def
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
|
| 9 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
return JsonResponse({"message": "Datos guardados exitosamente"})
|
|
|
|
| 11 |
except Exception as e:
|
|
|
|
| 12 |
return JsonResponse({"error": "Error al procesar datos"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from django.http import JsonResponse
|
| 2 |
from django.http import HttpRequest
|
| 3 |
+
from django.db import transaction
|
| 4 |
+
from tecnicas.models import Calificacion, DatoPunto, Producto, Participacion
|
| 5 |
+
|
| 6 |
|
| 7 |
class RatingNappingController:
|
| 8 |
@staticmethod
|
| 9 |
+
def saveRatingCoordinates(request: HttpRequest, data: list):
|
| 10 |
+
participation = Participacion.objects.get(
|
| 11 |
+
id=request.session["id_participation"]
|
| 12 |
+
)
|
| 13 |
|
| 14 |
try:
|
| 15 |
+
with transaction.atomic():
|
| 16 |
+
products_map = RatingNappingController.getProductsMap(
|
| 17 |
+
participation.tecnica)
|
| 18 |
+
|
| 19 |
+
existing_ratings_map = RatingNappingController.getExistingRatingsMap(
|
| 20 |
+
participation.tecnica, participation.catador
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
new_ratings = []
|
| 24 |
+
ids_products = products_map.keys()
|
| 25 |
+
for item in data:
|
| 26 |
+
product_id = int(item["idProduct"])
|
| 27 |
+
if product_id not in existing_ratings_map and product_id in ids_products:
|
| 28 |
+
new_ratings.append(
|
| 29 |
+
Calificacion(
|
| 30 |
+
num_repeticion=0,
|
| 31 |
+
id_producto=products_map[product_id],
|
| 32 |
+
id_tecnica=participation.tecnica,
|
| 33 |
+
id_catador=participation.catador,
|
| 34 |
+
)
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
if new_ratings:
|
| 38 |
+
Calificacion.objects.bulk_create(new_ratings)
|
| 39 |
+
existing_ratings_map = RatingNappingController.getExistingRatingsMap(
|
| 40 |
+
participation.tecnica, participation.catador
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
existing_points_map = RatingNappingController.getExistingPointsMap(
|
| 44 |
+
existing_ratings_map.values())
|
| 45 |
+
|
| 46 |
+
points_to_create = []
|
| 47 |
+
points_to_update = []
|
| 48 |
+
|
| 49 |
+
for item in data:
|
| 50 |
+
product_id = int(item["idProduct"])
|
| 51 |
+
rating = existing_ratings_map.get(product_id)
|
| 52 |
+
|
| 53 |
+
if rating:
|
| 54 |
+
if rating.id in existing_points_map:
|
| 55 |
+
point = existing_points_map[rating.id]
|
| 56 |
+
point.x = item["x"]
|
| 57 |
+
point.y = item["y"]
|
| 58 |
+
points_to_update.append(point)
|
| 59 |
+
else:
|
| 60 |
+
points_to_create.append(
|
| 61 |
+
DatoPunto(
|
| 62 |
+
x=item["x"],
|
| 63 |
+
y=item["y"],
|
| 64 |
+
calificacion=rating,
|
| 65 |
+
)
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
if points_to_create:
|
| 69 |
+
DatoPunto.objects.bulk_create(points_to_create)
|
| 70 |
+
|
| 71 |
+
if points_to_update:
|
| 72 |
+
DatoPunto.objects.bulk_update(points_to_update, ['x', 'y'])
|
| 73 |
+
|
| 74 |
return JsonResponse({"message": "Datos guardados exitosamente"})
|
| 75 |
+
|
| 76 |
except Exception as e:
|
| 77 |
+
print("ERROR:", e)
|
| 78 |
return JsonResponse({"error": "Error al procesar datos"})
|
| 79 |
+
|
| 80 |
+
@staticmethod
|
| 81 |
+
def getProductsMap(id_tecnica):
|
| 82 |
+
products_qs = Producto.objects.filter(id_tecnica=id_tecnica)
|
| 83 |
+
return {p.id: p for p in products_qs}
|
| 84 |
+
|
| 85 |
+
@staticmethod
|
| 86 |
+
def getExistingRatingsMap(id_tecnica, id_catador):
|
| 87 |
+
ratings = Calificacion.objects.filter(
|
| 88 |
+
id_tecnica=id_tecnica,
|
| 89 |
+
id_catador=id_catador,
|
| 90 |
+
)
|
| 91 |
+
return {r.id_producto.id: r for r in ratings}
|
| 92 |
+
|
| 93 |
+
@staticmethod
|
| 94 |
+
def getExistingPointsMap(ratings):
|
| 95 |
+
points = DatoPunto.objects.filter(calificacion__in=ratings)
|
| 96 |
+
return {p.calificacion.id: p for p in points}
|
tecnicas/controllers/views_controller/sessions_tester/tests_forms/test_napping_controller.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
from django.http import HttpRequest
|
| 2 |
from django.shortcuts import redirect, render
|
| 3 |
from django.urls import reverse
|
| 4 |
-
from
|
|
|
|
| 5 |
from tecnicas.utils import noValidTechnique
|
| 6 |
from .general_test_controller import GenetalTestController
|
| 7 |
|
|
@@ -48,4 +49,26 @@ class TestNappingController(GenetalTestController):
|
|
| 48 |
products_in_technique = Producto.objects.filter(id_tecnica=technique)
|
| 49 |
self.context["products"] = products_in_technique
|
| 50 |
|
|
|
|
|
|
|
| 51 |
return render(request, self.napping_test, self.context)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from django.http import HttpRequest
|
| 2 |
from django.shortcuts import redirect, render
|
| 3 |
from django.urls import reverse
|
| 4 |
+
from django.db.models import F
|
| 5 |
+
from tecnicas.models import Participacion, Producto, TecnicaModalidad, DatoPunto, Calificacion
|
| 6 |
from tecnicas.utils import noValidTechnique
|
| 7 |
from .general_test_controller import GenetalTestController
|
| 8 |
|
|
|
|
| 49 |
products_in_technique = Producto.objects.filter(id_tecnica=technique)
|
| 50 |
self.context["products"] = products_in_technique
|
| 51 |
|
| 52 |
+
self.setCoordinates()
|
| 53 |
+
|
| 54 |
return render(request, self.napping_test, self.context)
|
| 55 |
+
|
| 56 |
+
def setCoordinates(self):
|
| 57 |
+
technique = self.session.tecnica
|
| 58 |
+
|
| 59 |
+
ratings = Calificacion.objects.filter(
|
| 60 |
+
num_repeticion=0,
|
| 61 |
+
id_tecnica=technique,
|
| 62 |
+
id_catador=self.participation.catador
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
data_points = DatoPunto.objects.filter(
|
| 66 |
+
calificacion__in=ratings
|
| 67 |
+
).values(
|
| 68 |
+
code= F("calificacion__id_producto__codigoProducto"),
|
| 69 |
+
px= F("x"),
|
| 70 |
+
py= F("y"),
|
| 71 |
+
id_product= F("calificacion__id_producto")
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
self.context["data_points"] = list(data_points)
|
tecnicas/static/js/test-napping.js
CHANGED
|
@@ -81,14 +81,38 @@ function renderPoint(code, xPx, yPx, xVal, yVal) {
|
|
| 81 |
point.appendChild(label);
|
| 82 |
planeContainer.appendChild(point);
|
| 83 |
|
| 84 |
-
// Also show a permanent label next to the point if desired,
|
| 85 |
-
// or just rely on the hover. For now, let's add a small text label below it.
|
| 86 |
const textLabel = document.createElement('span');
|
| 87 |
textLabel.className = 'absolute top-4 left-1/2 transform -translate-x-1/2 text-xs font-bold text-gray-700 pointer-events-none';
|
| 88 |
textLabel.innerText = code;
|
| 89 |
point.appendChild(textLabel);
|
| 90 |
}
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
/*
|
| 93 |
////
|
| 94 |
//////
|
|
|
|
| 81 |
point.appendChild(label);
|
| 82 |
planeContainer.appendChild(point);
|
| 83 |
|
|
|
|
|
|
|
| 84 |
const textLabel = document.createElement('span');
|
| 85 |
textLabel.className = 'absolute top-4 left-1/2 transform -translate-x-1/2 text-xs font-bold text-gray-700 pointer-events-none';
|
| 86 |
textLabel.innerText = code;
|
| 87 |
point.appendChild(textLabel);
|
| 88 |
}
|
| 89 |
|
| 90 |
+
function checkPoints() {
|
| 91 |
+
const points = document.querySelectorAll('.data-point');
|
| 92 |
+
|
| 93 |
+
points.forEach(point => {
|
| 94 |
+
const code = point.dataset.code;
|
| 95 |
+
|
| 96 |
+
const xVal = parseFloat(point.dataset.px);
|
| 97 |
+
const yVal = parseFloat(point.dataset.py);
|
| 98 |
+
|
| 99 |
+
const rect = planeContainer.getBoundingClientRect();
|
| 100 |
+
|
| 101 |
+
const px = (xVal / PHYSICAL_WIDTH) * rect.width;
|
| 102 |
+
const py = ((PHYSICAL_HEIGHT - yVal) / PHYSICAL_HEIGHT) * rect.height;
|
| 103 |
+
|
| 104 |
+
placedPoints[code] = {
|
| 105 |
+
x: parseFloat(xVal.toFixed(2)),
|
| 106 |
+
y: parseFloat(yVal.toFixed(2)),
|
| 107 |
+
id: point.dataset.idProduct
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
renderPoint(code, px, py, xVal, yVal);
|
| 111 |
+
});
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
checkPoints();
|
| 115 |
+
|
| 116 |
/*
|
| 117 |
////
|
| 118 |
//////
|
tecnicas/templates/tecnicas/forms_tester/test_napping.html
CHANGED
|
@@ -64,10 +64,28 @@
|
|
| 64 |
<div class="relative w-full max-w-[800px] aspect-[3/2] bg-white border-2 border-gray-400 shadow-inner cursor-crosshair"
|
| 65 |
id="napping-plane"
|
| 66 |
style="background-image: linear-gradient(#e5e7eb 1px, transparent 1px), linear-gradient(90deg, #e5e7eb 1px, transparent 1px); background-size: 20px 20px;">
|
| 67 |
-
|
| 68 |
<span class="absolute bottom-1 right-2 text-xs text-gray-500 font-bold">60cm (X)</span>
|
| 69 |
<span class="absolute top-2 left-1 text-xs text-gray-500 font-bold">40cm (Y)</span>
|
| 70 |
<span class="absolute bottom-1 left-1 text-xs text-gray-500 font-bold">0</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
</div>
|
| 72 |
</div>
|
| 73 |
|
|
@@ -128,4 +146,31 @@
|
|
| 128 |
{% block extra_js %}
|
| 129 |
<script src="{% static 'js/actions-form.js' %}"></script>
|
| 130 |
<script src="{% static 'js/test-napping.js' %}"></script>
|
| 131 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
<div class="relative w-full max-w-[800px] aspect-[3/2] bg-white border-2 border-gray-400 shadow-inner cursor-crosshair"
|
| 65 |
id="napping-plane"
|
| 66 |
style="background-image: linear-gradient(#e5e7eb 1px, transparent 1px), linear-gradient(90deg, #e5e7eb 1px, transparent 1px); background-size: 20px 20px;">
|
| 67 |
+
|
| 68 |
<span class="absolute bottom-1 right-2 text-xs text-gray-500 font-bold">60cm (X)</span>
|
| 69 |
<span class="absolute top-2 left-1 text-xs text-gray-500 font-bold">40cm (Y)</span>
|
| 70 |
<span class="absolute bottom-1 left-1 text-xs text-gray-500 font-bold">0</span>
|
| 71 |
+
|
| 72 |
+
{% for point in data_points %}
|
| 73 |
+
<div id="point-{{ point.code }}"
|
| 74 |
+
class="data-point absolute w-4 h-4 bg-red-600 rounded-full transform -translate-x-1/2 -translate-y-1/2 cursor-pointer border-2 border-white shadow-md group"
|
| 75 |
+
data-px="{{ point.px }}" data-py="{{ point.py }}" data-code="{{ point.code }}"
|
| 76 |
+
data-id-product="{{ point.id_product }}">
|
| 77 |
+
<div
|
| 78 |
+
class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-800 text-white text-xs rounded whitespace-nowrap z-10 hidden group-hover:block">
|
| 79 |
+
<strong>{{ point.code }}</strong><br>
|
| 80 |
+
X: {{ point.px }}<br>
|
| 81 |
+
Y: {{ point.py }}
|
| 82 |
+
</div>
|
| 83 |
+
<span
|
| 84 |
+
class="absolute top-4 left-1/2 transform -translate-x-1/2 text-xs font-bold text-gray-700 pointer-events-none">
|
| 85 |
+
{{ point.code }}
|
| 86 |
+
</span>
|
| 87 |
+
</div>
|
| 88 |
+
{% endfor %}
|
| 89 |
</div>
|
| 90 |
</div>
|
| 91 |
|
|
|
|
| 146 |
{% block extra_js %}
|
| 147 |
<script src="{% static 'js/actions-form.js' %}"></script>
|
| 148 |
<script src="{% static 'js/test-napping.js' %}"></script>
|
| 149 |
+
{% endblock %}
|
| 150 |
+
|
| 151 |
+
<div id="point-JOW"
|
| 152 |
+
class="absolute w-4 h-4 bg-red-600 rounded-full transform -translate-x-1/2 -translate-y-1/2 cursor-pointer border-2 border-white shadow-md group"
|
| 153 |
+
style="left: 59px; top: 39px;">
|
| 154 |
+
<div
|
| 155 |
+
class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-800 text-white text-xs rounded whitespace-nowrap z-10 hidden group-hover:block">
|
| 156 |
+
<strong>JOW</strong><br>
|
| 157 |
+
X: 4.4<br>
|
| 158 |
+
Y: 37.1
|
| 159 |
+
</div><span
|
| 160 |
+
class="absolute top-4 left-1/2 transform -translate-x-1/2 text-xs font-bold text-gray-700 pointer-events-none">JOW</span>
|
| 161 |
+
</div>
|
| 162 |
+
|
| 163 |
+
<div id="point-JOW"
|
| 164 |
+
class="absolute w-4 h-4 bg-red-600 rounded-full transform -translate-x-1/2 -translate-y-1/2 cursor-pointer border-2 border-white shadow-md group"
|
| 165 |
+
style="left: 58.93px; top: 685.07px;">
|
| 166 |
+
<div
|
| 167 |
+
class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-800 text-white text-xs rounded whitespace-nowrap z-10 hidden group-hover:block">
|
| 168 |
+
<strong>JOW</strong><br>
|
| 169 |
+
X: 4.4<br>
|
| 170 |
+
Y: 37.1
|
| 171 |
+
</div>
|
| 172 |
+
<span
|
| 173 |
+
class="absolute top-4 left-1/2 transform -translate-x-1/2 text-xs font-bold text-gray-700 pointer-events-none">
|
| 174 |
+
JOW
|
| 175 |
+
</span>
|
| 176 |
+
</div>
|
tecnicas/views/apis/rating_napping.py
CHANGED
|
@@ -7,7 +7,7 @@ def ratingNappingNoMode(req: HttpRequest):
|
|
| 7 |
if req.method == "POST":
|
| 8 |
try:
|
| 9 |
data = json.loads(req.body.decode("utf-8"))
|
| 10 |
-
response = RatingNappingController.
|
| 11 |
request=req, data=data)
|
| 12 |
return response
|
| 13 |
except Exception as e:
|
|
|
|
| 7 |
if req.method == "POST":
|
| 8 |
try:
|
| 9 |
data = json.loads(req.body.decode("utf-8"))
|
| 10 |
+
response = RatingNappingController.saveRatingCoordinates(
|
| 11 |
request=req, data=data)
|
| 12 |
return response
|
| 13 |
except Exception as e:
|