Norberto Montalvo García commited on
Commit
7be41d4
·
unverified ·
2 Parent(s): e49f021 eade001

Merge pull request #37 from CascoArcilla/HU10

Browse files
tecnicas/controllers/api_controller/rating_pf_list_controller.py CHANGED
@@ -1,6 +1,6 @@
1
  from django.http import JsonResponse, HttpRequest
2
  from django.db import transaction, IntegrityError
3
- from tecnicas.models import Palabra, ListaPalabras, Participacion
4
  from tecnicas.forms import ListWordsForm
5
 
6
 
@@ -141,3 +141,81 @@ class RatingPFListController():
141
  list_tester.palabras.set(all_new_words)
142
 
143
  return all_new_words
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from django.http import JsonResponse, HttpRequest
2
  from django.db import transaction, IntegrityError
3
+ from tecnicas.models import Palabra, ListaPalabras, Participacion, Calificacion, Producto, Dato, ValorDecimal
4
  from tecnicas.forms import ListWordsForm
5
 
6
 
 
141
  list_tester.palabras.set(all_new_words)
142
 
143
  return all_new_words
144
+
145
+ @staticmethod
146
+ def saveRatings(request: HttpRequest, word_rating: str, data: list):
147
+ participation = Participacion.objects.get(
148
+ id=request.session["id_participation"])
149
+
150
+ technique = participation.tecnica
151
+ products = Producto.objects.filter(id_tecnica=technique)
152
+
153
+ # Crear o obtener las instancias Calificacion en la repeticion de todos los producutos
154
+ ratings = Calificacion.objects.filter(
155
+ num_repeticion=technique.repeticion,
156
+ id_tecnica=technique,
157
+ id_catador=participation.catador,
158
+ id_producto__in=products
159
+ )
160
+
161
+ existing_dict = {rating.id_producto_id: rating for rating in ratings}
162
+ to_create = []
163
+
164
+ for product in products:
165
+ if product.id not in existing_dict:
166
+ to_create.append(
167
+ Calificacion(
168
+ num_repeticion=technique.repeticion,
169
+ id_producto=product,
170
+ id_tecnica=technique,
171
+ id_catador=participation.catador
172
+ )
173
+ )
174
+ Calificacion.objects.bulk_create(to_create)
175
+
176
+ ratings = Calificacion.objects.filter(
177
+ num_repeticion=technique.repeticion,
178
+ id_tecnica=technique,
179
+ id_catador=participation.catador,
180
+ id_producto__in=products
181
+ )
182
+
183
+ # Guardar datos con ValorDecimal
184
+ word = Palabra.objects.get(nombre_palabra=word_rating)
185
+ try:
186
+ if (
187
+ len(data) != len(products) or
188
+ len(data) != len(ratings) or
189
+ len(products) != len(ratings)
190
+ ):
191
+ raise ValueError(
192
+ "Al parecer los datos mandados no corresponden con el total de productos")
193
+
194
+ product_values = {info["product"]["code"]: info["value"] for info in data}
195
+
196
+ datos_to_create = []
197
+ values_to_create = []
198
+
199
+ with transaction.atomic():
200
+ for rating in ratings:
201
+ datos_to_create.append(Dato(
202
+ id_palabra=word,
203
+ id_calificacion=rating
204
+ ))
205
+
206
+ datos_save = Dato.objects.bulk_create(datos_to_create)
207
+ save_data = Dato.objects.filter(id_palabra=word, id_calificacion__in=ratings)
208
+
209
+ for data_save in save_data:
210
+ values_to_create.append(ValorDecimal(
211
+ id_dato=data_save,
212
+ valor=product_values.get(data_save.id_calificacion.id_producto.codigoProducto)
213
+ ))
214
+
215
+ ValorDecimal.objects.bulk_create(values_to_create)
216
+
217
+ return JsonResponse({"message": "Calificaciones guardadas con exito"})
218
+ except ValueError as e:
219
+ error_message = str(e)
220
+ print(f"Error de calificacion: {error_message}")
221
+ return JsonResponse({"error": error_message})
tecnicas/controllers/views_controller/session_management/details_controller.py CHANGED
@@ -35,11 +35,11 @@ class DetallesController():
35
  technique = self.session.tecnica
36
 
37
  if creator.user.username != self.session.creadoPor.user.username:
38
- return self.getResponse(error="Solo el presentador que crea la sesión puede iniciar la repetición", request=request)
39
  elif self.session.activo:
40
- return self.getResponse(error="La sesión ya está activada", request=request)
41
  elif technique.repeticion >= technique.repeticiones_max:
42
- return self.getResponse(error="Se ha alcanzado el número de repeticiones máxima", request=request)
43
 
44
  there_participacions = Participacion.objects.filter(
45
  tecnica=technique).exists()
@@ -48,7 +48,7 @@ class DetallesController():
48
  (is_update_participations,
49
  message) = ParticipacionController.outAllInSession(self.session)
50
  if not is_update_participations:
51
- return self.getResponse(error=message, request=request)
52
 
53
  self.session.activo = True
54
  technique.repeticion = technique.repeticion + 1
 
35
  technique = self.session.tecnica
36
 
37
  if creator.user.username != self.session.creadoPor.user.username:
38
+ return self.controllGetResponse(error="Solo el presentador que crea la sesión puede iniciar la repetición", request=request)
39
  elif self.session.activo:
40
+ return self.controllGetResponse(error="La sesión ya está activada", request=request)
41
  elif technique.repeticion >= technique.repeticiones_max:
42
+ return self.controllGetResponse(error="Se ha alcanzado el número de repeticiones máxima", request=request)
43
 
44
  there_participacions = Participacion.objects.filter(
45
  tecnica=technique).exists()
 
48
  (is_update_participations,
49
  message) = ParticipacionController.outAllInSession(self.session)
50
  if not is_update_participations:
51
+ return self.controllGetResponse(error=message, request=request)
52
 
53
  self.session.activo = True
54
  technique.repeticion = technique.repeticion + 1
tecnicas/controllers/views_controller/session_management/details_pf_controller.py CHANGED
@@ -20,10 +20,10 @@ class DetallesPFController(DetallesController):
20
  self.context = {
21
  "sesion": self.session,
22
  "use_technique": technique,
23
- "calificaciones": [],
24
  "existen_calificaciones": False,
25
  "tipo_escala": technique.escala_tecnica.id_tipo_escala.nombre_escala,
26
- "valor_max": technique.escala_tecnica.longitud
 
27
  }
28
 
29
  # Definir el estado de la sesion
@@ -35,48 +35,23 @@ class DetallesPFController(DetallesController):
35
 
36
  return self.context
37
 
38
- def startRepetition(self, presenter: Presentador, request: HttpRequest):
39
- creator = presenter
40
- technique = self.session.tecnica
41
-
42
- if creator.user.username != self.session.creadoPor.user.username:
43
- return self.getResponse(error="Solo el presentador que crea la sesión puede iniciar la repetición o fase", request=request)
44
- elif self.session.activo:
45
- return self.getResponse(error="La sesión ya está activada", request=request)
46
-
47
- there_participacions = Participacion.objects.filter(
48
- tecnica=technique).exists()
49
-
50
- if there_participacions:
51
- (is_update_participations,
52
- message) = ParticipacionController.outAllInSession(self.session)
53
- if not is_update_participations:
54
- return self.getResponse(error=message, request=request)
55
-
56
- self.session.activo = True
57
- technique.repeticion = technique.repeticion + 1
58
-
59
- technique.save()
60
- self.session.save()
61
-
62
- parameters = {
63
- "session_code": self.session.codigo_sesion
64
- }
65
- return redirect(
66
- reverse(self.url_next, kwargs=parameters))
67
-
68
  def getDataPhases(self):
69
  curren_repetition = self.session.tecnica.repeticion
70
 
71
  if curren_repetition == 1:
72
  self.context["fisrt_phase"] = self.getDataFirstPhase()
 
 
73
  elif curren_repetition == 2:
74
  self.context["fisrt_phase"] = self.getDataFirstPhase()
75
  self.context["second_phase"] = self.getDataSecondPhase()
 
 
76
  elif curren_repetition >= 3:
77
  self.context["fisrt_phase"] = self.getDataFirstPhase()
78
  self.context["second_phase"] = self.getDataSecondPhase()
79
  self.context["data_ratings"] = self.getDataRatings()
 
80
 
81
  return self.context
82
 
@@ -135,10 +110,64 @@ class DetallesPFController(DetallesController):
135
  'username': username,
136
  'words': words
137
  })
138
-
139
  return result
140
 
141
  def getDataRatings(self):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  lists_words_testers = self.context["second_phase"]
143
  technique = self.session.tecnica
144
 
@@ -160,6 +189,7 @@ class DetallesPFController(DetallesController):
160
 
161
  ratings_for_repetition = defaultdict(lambda: defaultdict(list))
162
 
 
163
  for item in data:
164
  rep = item["repeticion"]
165
  prod = item["producto_code"]
@@ -167,7 +197,7 @@ class DetallesPFController(DetallesController):
167
  ratings_for_repetition[rep-2][prod].append({
168
  "nombre_palabra": item["nombre_palabra"],
169
  "dato_valor": item["dato_valor"]
170
- })
171
 
172
  ratings_for_tester.append(
173
  {
@@ -177,29 +207,5 @@ class DetallesPFController(DetallesController):
177
  "words": list_tester["words"]
178
  }
179
  )
180
- self.context["existen_calificaciones"] = True
181
-
182
- return ratings_for_tester
183
-
184
- def getStatus(self, rep: int, activate: bool):
185
- status = ""
186
-
187
- if rep == 0 and not activate:
188
- status = "Listo para crear listas iniciales"
189
 
190
- elif rep == 1 and activate:
191
- status = "En primera fase, creación de listas iniciales"
192
- elif rep == 1 and not activate:
193
- status = "Listo para crear listas finales"
194
-
195
- elif rep == 2 and activate:
196
- status = "En segunda fase, creación de listas finales"
197
- elif rep == 2 and not activate:
198
- status = "Listo para calificaciones"
199
-
200
- elif rep > 2 and not activate:
201
- status = "Listo para calificaciones"
202
- elif rep > 2 and activate:
203
- status = "Catadores calificando"
204
-
205
- return status
 
20
  self.context = {
21
  "sesion": self.session,
22
  "use_technique": technique,
 
23
  "existen_calificaciones": False,
24
  "tipo_escala": technique.escala_tecnica.id_tipo_escala.nombre_escala,
25
+ "valor_max": technique.escala_tecnica.longitud,
26
+ "repeticiones_max": technique.repeticiones_max - 2
27
  }
28
 
29
  # Definir el estado de la sesion
 
35
 
36
  return self.context
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  def getDataPhases(self):
39
  curren_repetition = self.session.tecnica.repeticion
40
 
41
  if curren_repetition == 1:
42
  self.context["fisrt_phase"] = self.getDataFirstPhase()
43
+ self.context["repeticion"] = 0
44
+
45
  elif curren_repetition == 2:
46
  self.context["fisrt_phase"] = self.getDataFirstPhase()
47
  self.context["second_phase"] = self.getDataSecondPhase()
48
+ self.context["repeticion"] = 0
49
+
50
  elif curren_repetition >= 3:
51
  self.context["fisrt_phase"] = self.getDataFirstPhase()
52
  self.context["second_phase"] = self.getDataSecondPhase()
53
  self.context["data_ratings"] = self.getDataRatings()
54
+ self.context["repeticion"] = self.session.tecnica.repeticion - 2
55
 
56
  return self.context
57
 
 
110
  'username': username,
111
  'words': words
112
  })
113
+
114
  return result
115
 
116
  def getDataRatings(self):
117
+ technique = self.session.tecnica
118
+
119
+ if technique.repeticion > 3:
120
+ return self.getDataRatingsFinal()
121
+
122
+ elif technique.repeticion == 3:
123
+ return self.getDataRatingsInitial()
124
+
125
+ def getStatus(self, rep: int, activate: bool):
126
+ status = ""
127
+
128
+ if rep == 0 and not activate:
129
+ status = "Listo para crear listas iniciales"
130
+
131
+ elif rep == 1 and activate:
132
+ status = "En primera fase, creación de listas iniciales"
133
+ elif rep == 1 and not activate:
134
+ status = "Listo para crear listas finales"
135
+
136
+ elif rep == 2 and activate:
137
+ status = "En segunda fase, creación de listas finales"
138
+ elif rep == 2 and not activate:
139
+ status = "Listo para calificaciones"
140
+
141
+ elif rep > 2 and not activate:
142
+ status = "Listo para calificaciones"
143
+ elif rep > 2 and activate:
144
+ status = "Catadores calificando"
145
+
146
+ return status
147
+
148
+ def getDataRatingsInitial(self):
149
+ ratings = list(Calificacion.objects.filter(id_tecnica=self.session.tecnica, num_repeticion=3))
150
+
151
+ if ratings:
152
+ raw_data = DatoController.getWordValuesForConvecional(
153
+ technique=self.session.tecnica,
154
+ ratings=ratings
155
+ )
156
+
157
+ structured_data = defaultdict(lambda: defaultdict(list))
158
+
159
+ for item in raw_data:
160
+ prod_code = item["producto_code"]
161
+ username = item["usuario_catador"]
162
+
163
+ structured_data[prod_code][username].append({
164
+ "palabra": item["nombre_palabra"],
165
+ "valor": item["dato_valor"]
166
+ })
167
+
168
+ return defaultdict_to_dict(structured_data)
169
+
170
+ def getDataRatingsFinal(self):
171
  lists_words_testers = self.context["second_phase"]
172
  technique = self.session.tecnica
173
 
 
189
 
190
  ratings_for_repetition = defaultdict(lambda: defaultdict(list))
191
 
192
+ # Estructurar los datos
193
  for item in data:
194
  rep = item["repeticion"]
195
  prod = item["producto_code"]
 
197
  ratings_for_repetition[rep-2][prod].append({
198
  "nombre_palabra": item["nombre_palabra"],
199
  "dato_valor": item["dato_valor"]
200
+ })
201
 
202
  ratings_for_tester.append(
203
  {
 
207
  "words": list_tester["words"]
208
  }
209
  )
 
 
 
 
 
 
 
 
 
210
 
211
+ return ratings_for_tester
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tecnicas/static/js/download-table-xlsx.js ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener("DOMContentLoaded", function () {
2
+ const btn = document.getElementById("download-xls-btn");
3
+ if (!btn) return;
4
+
5
+ btn.addEventListener("click", async function () {
6
+ if (typeof XLSX === "undefined") {
7
+ alert(
8
+ "Librerías XLSX no están cargadas."
9
+ );
10
+ return;
11
+ }
12
+
13
+ const BOOK = XLSX.utils.book_new();
14
+
15
+ const sections = document.querySelectorAll("section[data-tester]");
16
+ if (!sections || sections.length === 0) {
17
+ alert("No hay tablas para descargar.");
18
+ return;
19
+ }
20
+
21
+ let fileXlsxName = "";
22
+
23
+ sections.forEach((sec) => {
24
+ const tester = sec.getAttribute("data-tester") || "tester";
25
+ const sessionName = sec.getAttribute("data-session-name") || "";
26
+ const sessionCode = sec.getAttribute("data-session-code") || "";
27
+
28
+ const sheetName = `${tester}`;
29
+
30
+ fileXlsxName = `datos_sesion_${sessionName ? sessionName.trim() : sessionCode.trim()
31
+ }`;
32
+
33
+ const table = sec.querySelector("table");
34
+ if (!table) return;
35
+
36
+ // Build CSV
37
+ const rows = [];
38
+ const headerCells = Array.from(table.querySelectorAll("thead tr th"));
39
+ const headers = headerCells.map((h) => h.textContent.trim());
40
+ rows.push(headers.map(escapeCsv).join(","));
41
+
42
+ const trs = table.querySelectorAll("tbody tr");
43
+ trs.forEach((tr) => {
44
+ const tds = Array.from(tr.querySelectorAll("td"));
45
+ const values = tds.map((td) => escapeCsv(td.textContent.trim()));
46
+ rows.push(values.join(","));
47
+ });
48
+
49
+ const csvContent = rows.join("\r\n");
50
+ const sheet = XLSX.read(csvContent, { type: "string" }).Sheets["Sheet1"];
51
+ XLSX.utils.book_append_sheet(BOOK, sheet, sheetName);
52
+ });
53
+
54
+ try {
55
+ XLSX.writeFile(BOOK, `${fileXlsxName}.xlsx`);
56
+ } catch (err) {
57
+ console.error(err);
58
+ alert("Error al generar el archivo XLSX: " + err.message);
59
+ }
60
+ });
61
+
62
+ const escapeCsv = (val) => {
63
+ if (val == null) return "";
64
+ let normalVal = val.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
65
+ const needsQuotes = /[",\n,]/.test(normalVal);
66
+ let v = String(normalVal).replace(/"/g, '""');
67
+ if (needsQuotes) v = `"${v}"`;
68
+ return v;
69
+ };
70
+ });
tecnicas/static/js/span-notification.js ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function spanNotifaction(messageError, isError = true, time = 4500) {
2
+ const span = document.createElement("span");
3
+ span.textContent = messageError;
4
+
5
+ const div = document.createElement("div");
6
+ div.classList.add("alert", isError ? "alert-error" : "alert-success");
7
+ div.appendChild(span);
8
+
9
+ document.querySelector(".toast").append(div);
10
+
11
+ setTimeout(() => {
12
+ document.querySelector(".toast").removeChild(div);
13
+ }, time);
14
+ }
tecnicas/static/js/test_pf_rating_list.js ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let dragged = null;
2
+ const nextProduct = `
3
+ <h2 class="text-2xl font-bold text-center">Éxito al guardar los datos</h2>
4
+ <div class="flex justify-center">
5
+ <button class="cts-btn-general cts-btn-primary btn-push" onclick="window.location.reload()">
6
+ Evaluar siguiente atributo
7
+ </button>
8
+ </div>`;
9
+
10
+ document.querySelectorAll(".draggable").forEach((el) => {
11
+ el.addEventListener("dragstart", () => {
12
+ dragged = el;
13
+ setTimeout(() => el.classList.add("opacity-50"), 0);
14
+ });
15
+
16
+ el.addEventListener("dragend", () => {
17
+ dragged = null;
18
+ el.classList.remove("opacity-50");
19
+ });
20
+ });
21
+
22
+ document.querySelectorAll(".dropzone").forEach((zone) => {
23
+ zone.addEventListener("dragover", (e) => e.preventDefault());
24
+
25
+ zone.addEventListener("drop", () => {
26
+ zone.appendChild(dragged);
27
+ });
28
+ });
29
+
30
+ document
31
+ .getElementById("question-save")
32
+ .addEventListener("click", showOptionsSave);
33
+
34
+ document
35
+ .getElementById("cancel-save")
36
+ .addEventListener("click", showQuestionSave);
37
+
38
+ document.getElementById("save-data").addEventListener("click", async () => {
39
+ showLoading();
40
+ const currentDataRatend = [];
41
+
42
+ document.querySelectorAll(".dropzone").forEach((zone) => {
43
+ const index = parseInt(zone.dataset.index);
44
+ const children = zone.querySelectorAll(".draggable");
45
+
46
+ children.forEach((el) => {
47
+ currentDataRatend.push({
48
+ product: {
49
+ id: el.dataset.idProduct,
50
+ code: el.dataset.code,
51
+ },
52
+ value: index,
53
+ });
54
+ });
55
+ });
56
+
57
+ if (!currentDataRatend.length) {
58
+ cancelLoading();
59
+ spanNotifaction("No has ordenado los productos");
60
+ } else if (
61
+ currentDataRatend.length != document.querySelectorAll(".draggable").length
62
+ ) {
63
+ cancelLoading();
64
+ spanNotifaction("Faltan productos por calificar");
65
+ } else {
66
+ saveData(currentDataRatend);
67
+ }
68
+ });
69
+
70
+ async function saveData(dataToSend = []) {
71
+ const URL = "/cata/testers/api/ratingword/pf/list";
72
+
73
+ const csrfToken = document.querySelector("[name=csrfmiddlewaretoken]").value;
74
+
75
+ const requestData = {
76
+ phase: parseInt(document.querySelector(".cts-phase-pf").dataset.phase),
77
+ word: document.querySelector(".cts-word-rating").dataset.nameWord,
78
+ data: dataToSend,
79
+ };
80
+
81
+ try {
82
+ const response = await fetch(URL, {
83
+ method: "POST",
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ "X-CSRFToken": csrfToken,
87
+ "X-Requested-With": "XMLHttpRequest",
88
+ },
89
+ body: JSON.stringify(requestData),
90
+ });
91
+
92
+ if (!response.ok) {
93
+ spanNotifaction("Fallo con la respuesta recibida");
94
+ cancelLoading();
95
+ return false;
96
+ }
97
+
98
+ const result = await response.json();
99
+
100
+ const messError = result.error;
101
+
102
+ if (messError) {
103
+ spanNotifaction(messError);
104
+ cancelLoading();
105
+ return false;
106
+ }
107
+
108
+ spanNotifaction(result.message, false);
109
+ const containerRatings = document.querySelector(".container-rating-word");
110
+ containerRatings.innerHTML = "";
111
+ containerRatings.innerHTML = nextProduct;
112
+ cancelLoading();
113
+ return true;
114
+ } catch (error) {
115
+ console.log(error);
116
+ cancelLoading();
117
+ spanNotifaction("¡Oh! Ocurrió un error al tratar de guardar los datos");
118
+ return false;
119
+ }
120
+ }
121
+
122
+ function showLoading() {
123
+ document.getElementById("save-data").classList.add("hidden");
124
+ document.getElementById("cancel-save").classList.add("hidden");
125
+ document.getElementById("loading-data-save").classList.remove("hidden");
126
+ }
127
+
128
+ function cancelLoading() {
129
+ document.getElementById("loading-data-save").classList.add("hidden");
130
+ document.getElementById("question-save").classList.remove("hidden");
131
+ }
132
+
133
+ function showOptionsSave() {
134
+ document.getElementById("question-save").classList.add("hidden");
135
+ document.getElementById("save-data").classList.remove("hidden");
136
+ document.getElementById("cancel-save").classList.remove("hidden");
137
+ }
138
+
139
+ function showQuestionSave() {
140
+ document.getElementById("question-save").classList.remove("hidden");
141
+ document.getElementById("save-data").classList.add("hidden");
142
+ document.getElementById("cancel-save").classList.add("hidden");
143
+ }
tecnicas/templates/tecnicas/components/table_pf_all.html ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% load custom_filters %}
2
+ <section>
3
+ <div class="overflow-x-auto rounded-lg border border-surface-general">
4
+ <table class="min-w-max w-full text-sm text-center border-collapse">
5
+ <thead class="bg-surface-sweet text-black font-semibold">
6
+ <tr>
7
+ <th rowspan="2" class="py-2 px-3 border border-surface-general">Producto</th>
8
+ {% for user_data in second_phase %}
9
+ <th colspan="{{ user_data.words|length }}" class="py-2 px-3 border border-surface-general">
10
+ {{ user_data.username }}
11
+ </th>
12
+ {% endfor %}
13
+ </tr>
14
+ <tr>
15
+ {% for user_data in second_phase %}
16
+ {% for word in user_data.words %}
17
+ <th class="py-2 px-3 border border-surface-general text-xs">
18
+ {{ word.nombre_palabra }}
19
+ </th>
20
+ {% endfor %}
21
+ {% endfor %}
22
+ </tr>
23
+ </thead>
24
+ <tbody class="bg-surface-ligt divide-y divide-gray-200">
25
+ {% for product_code, users_ratings in data_ratings.items %}
26
+ <tr>
27
+ <td class="py-2 px-3 border border-surface-general font-medium">{{ product_code }}</td>
28
+
29
+ {% for user_data in second_phase %}
30
+ {% with user_ratings=users_ratings|get_item:user_data.username %}
31
+ {% for word in user_data.words %}
32
+ <td class="py-2 px-3 border border-surface-general">
33
+ {% if user_ratings %}
34
+ {% for rating in user_ratings %}
35
+ {% if rating.palabra == word.nombre_palabra %}
36
+ {{ rating.valor }}
37
+ {% endif %}
38
+ {% endfor %}
39
+ {% else %}
40
+ -
41
+ {% endif %}
42
+ </td>
43
+ {% endfor %}
44
+ {% endwith %}
45
+ {% endfor %}
46
+ </tr>
47
+ {% endfor %}
48
+ </tbody>
49
+ </table>
50
+ </div>
51
+ </section>
tecnicas/templates/tecnicas/components/toast-container.html ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {% load static %}
2
+ <div class="toast toast-center"></div>
3
+ <script src="{% static 'js/span-notification.js' %}"></script>
tecnicas/templates/tecnicas/forms_tester/test_pf_rating_list.html CHANGED
@@ -4,37 +4,9 @@
4
 
5
  {% block title %}Convencional{% endblock %}
6
 
7
- {% block extra_css %}
8
- <style>
9
- input[type="range"]::-webkit-slider-thumb {
10
- -webkit-appearance: none;
11
- appearance: none;
12
- width: 24px;
13
- height: 24px;
14
- border-radius: 50%;
15
- background: var(--color-blue-500);
16
- cursor: pointer;
17
- }
18
-
19
- input[type="range"]::-moz-range-thumb {
20
- width: 24px;
21
- height: 24px;
22
- border-radius: 50%;
23
- background: #3b82f6;
24
- cursor: pointer;
25
- }
26
-
27
- @media (width < 40rem) {
28
- .container-forms {
29
- width: inherit;
30
- }
31
- }
32
- </style>
33
- {% endblock %}
34
-
35
  {% block content %}
36
  <article class="cts-container-main">
37
- <article class="cts-wrap-content text-black">
38
  <header class="text-center flex-row w-full items-stretch flex justify-around flex-wrap gap-2">
39
  <h1 class="rounded font-bold text-2xl bg-surface-ligt p-4 flex-1">
40
  Sesión usando <br>técnica
@@ -88,29 +60,72 @@
88
  </div>
89
  </section>
90
 
91
- <section class="flex flex-col bg-red-200 py-2 px-4 rounded-lg min-sm:hidden">
92
- <p>
93
- El largo de estas escalas puede afectar la vista en dispositivos móviles. Para ver el resto de la
94
- escala puede deslizar con el dedo a la derecha o la izquierda sin tocar la escala.
95
- </p>
 
 
 
 
96
  </section>
97
  </article>
98
 
99
- <article class="rounded flex flex-col gap-4">
100
- <p>Palabra a evaluar: {{ word }}</p>
101
- <div>
102
- <p>Productos a ordenar:</p>
103
- <ul>
 
 
 
 
 
 
 
 
 
104
  {% for product in products %}
105
- <li>{{ product }}</li>
 
 
 
 
106
  {% endfor %}
107
- </ul>
108
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  </article>
 
 
110
  </article>
111
  </article>
112
  {% endblock %}
113
 
114
  {% block extra_js %}
115
  <script src="{% static 'js/actions-form.js' %}"></script>
 
116
  {% endblock %}
 
4
 
5
  {% block title %}Convencional{% endblock %}
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  {% block content %}
8
  <article class="cts-container-main">
9
+ <article class="cts-wrap-content text-black max-w-4xl">
10
  <header class="text-center flex-row w-full items-stretch flex justify-around flex-wrap gap-2">
11
  <h1 class="rounded font-bold text-2xl bg-surface-ligt p-4 flex-1">
12
  Sesión usando <br>técnica
 
60
  </div>
61
  </section>
62
 
63
+ <section class="flex items-center justify-center flex-wrap gap-4">
64
+ <div class="bg-surface-card p-2 rounded-lg flex-1">
65
+ <p class="text-lg font-bold text-center">
66
+ El atributo que se está evaluando:
67
+ </p>
68
+ <p class="cts-word-rating text-2xl font-bold text-center" data-name-word="{{ word }}">
69
+ "{{ word }}"
70
+ </p>
71
+ </div>
72
  </section>
73
  </article>
74
 
75
+ <article class="container-rating-word p-6 space-y-6 bg-surface-ligt rounded">
76
+ <!-- Definir toddos los productos para la escala -->
77
+ <h2 class="text-xl font-bold">Productos en la sesión</h2>
78
+ <div id="items" class="flex gap-4 flex-wrap justify-center">
79
+ {% for product in products %}
80
+ <div class="draggable bg-surface-sweet text-black font-bold px-4 py-2 rounded cursor-grab"
81
+ draggable="true" data-id-product="{{ product.id }}" data-code="{{ product.codigoProducto }}">{{ product.codigoProducto }}</div>
82
+ {% endfor %}
83
+ </div>
84
+
85
+ <section>
86
+ <!-- Definir toddos los espacion de escala ordinaria -->
87
+ <h2 class="text-xl font-bold mb-6">Escala ordinaria</h2>
88
+ <div id="containers" class="flex gap-6 flex-wrap justify-center">
89
  {% for product in products %}
90
+ <section class="w-fit space-y-2">
91
+ <div class="dropzone w-32 min-h-16 bg-surface-alter-card border-2 border-dashed border-gray-500 rounded p-2 space-y-2"
92
+ data-index="{{ forloop.counter }}"></div>
93
+ <p class="text-center text-2xl">{{ forloop.counter }}</p>
94
+ </section>
95
  {% endfor %}
96
+ </div>
97
+
98
+ <!-- Linea de menor a mayor -->
99
+ <section>
100
+ <div class="flex justify-center items-center gap-4">
101
+ <span class="text-2xl">➖</span>
102
+ <hr class="border-2 flex-1">
103
+ <SPan class="text-2xl">➕</SPan>
104
+ </div>
105
+ <p class="text-center text-sm">Valores de menor a mayor</p>
106
+ </section>
107
+ </section>
108
+
109
+ <section class="flex justify-end gap-4">
110
+ <button id="question-save" class="cts-btn-general cts-btn-secondary btn-push">
111
+ ¿Guardar datos actuales?
112
+ </button>
113
+ <button id="save-data" class="cts-btn-general cts-btn-primary btn-push hidden">
114
+ Guardar
115
+ </button>
116
+ <button id="cancel-save" class="cts-btn-general cts-btn-error btn-push hidden">
117
+ Cancelar
118
+ </button>
119
+ <span id="loading-data-save" class="loading loading-spinner loading-xl text-accent hidden"></span>
120
+ </section>
121
  </article>
122
+
123
+ {% include "../components/toast-container.html" %}
124
  </article>
125
  </article>
126
  {% endblock %}
127
 
128
  {% block extra_js %}
129
  <script src="{% static 'js/actions-form.js' %}"></script>
130
+ <script src="{% static 'js/test_pf_rating_list.js' %}"></script>
131
  {% endblock %}
tecnicas/templates/tecnicas/manage_sesions/details-session-pf.html CHANGED
@@ -90,6 +90,20 @@
90
  </p>
91
  </section>
92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  <section
94
  class="col-span-2 max-sm:col-span-1 bg-surface-card flex flex-wrap items-center justify-center max-sm:justify-normal gap-x-2 p-4 rounded-2xl">
95
  <p class="font-bold">
@@ -205,7 +219,7 @@
205
  <section class="">
206
  <header class="text-center">
207
  <h3 class="text-xl font-bold">Tercera fase</h3>
208
- <p class="text-lg">Listas finales de Catadores</p>
209
  </header>
210
 
211
  <article class="text-xl space-y-4 space-x-4 mb-4">
@@ -217,19 +231,32 @@
217
  </p>
218
  </article>
219
 
220
- {% if data_ratings %}
221
- <article class="overflow-x-auto py-2 space-y-4">
222
- {% for data_tester in data_ratings %}
223
- {% include "../components/table_pf.html" with data=data_tester session_name=sesion.nombre_sesion session_code=sesion.codigo_sesion %}
224
- {% endfor %}
225
- </article>
226
- <div class="flex justify-end mt-3">
227
- <button id="download-csv-btn" class="cts-btn-general cts-btn-primary btn-push">
228
- Descargar datos como CSV en zip
229
- </button>
230
- </div>
231
  {% else %}
232
- {% include "../components/error-message.html" with message='Sin datos por mostrar aún' %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  {% endif %}
234
  </section>
235
  </article>
@@ -243,8 +270,10 @@
243
  {% endblock %}
244
 
245
  {% block extra_js %}
246
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
247
- <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
 
 
248
  <script src="{% static 'js/download-table-zip.js' %}"></script>
249
  <script src="{% static 'js/details-session.js' %}"></script>
250
  <script src="{% static 'js/showHiddenElement.js' %}"></script>
 
90
  </p>
91
  </section>
92
 
93
+ <section
94
+ class="bg-surface-card flex flex-wrap items-center justify-center max-sm:justify-normal gap-x-2 p-4 rounded-2xl">
95
+ <p class="font-sans">
96
+ <b>Repetición Actual</b> {{ repeticion }}
97
+ </p>
98
+ </section>
99
+
100
+ <section
101
+ class="bg-surface-card flex flex-wrap items-center justify-center max-sm:justify-normal gap-x-2 p-4 rounded-2xl">
102
+ <p class="font-sans">
103
+ <b>Repeticiones Max.</b> {{ repeticiones_max }}
104
+ </p>
105
+ </section>
106
+
107
  <section
108
  class="col-span-2 max-sm:col-span-1 bg-surface-card flex flex-wrap items-center justify-center max-sm:justify-normal gap-x-2 p-4 rounded-2xl">
109
  <p class="font-bold">
 
219
  <section class="">
220
  <header class="text-center">
221
  <h3 class="text-xl font-bold">Tercera fase</h3>
222
+ <p class="text-lg">Califiaciones de Catadores</p>
223
  </header>
224
 
225
  <article class="text-xl space-y-4 space-x-4 mb-4">
 
231
  </p>
232
  </article>
233
 
234
+ {% if repeticion == 1 %}
235
+ {% if data_ratings %}
236
+ {% include "../components/table_pf_all.html" with second_phase=second_phase data_ratings=data_ratings %}
237
+ {% else %}
238
+ {% include "../components/error-message.html" with message='Sin datos por mostrar aún' %}
239
+ {% endif %}
 
 
 
 
 
240
  {% else %}
241
+ {% if data_ratings %}
242
+ <article class="overflow-x-auto py-2 space-y-4">
243
+ {% for data_tester in data_ratings %}
244
+ {% include "../components/table_pf.html" with data=data_tester session_name=sesion.nombre_sesion session_code=sesion.codigo_sesion %}
245
+ {% endfor %}
246
+ </article>
247
+ <div class="flex justify-end mt-3 gap-4 flex-wrap">
248
+ <button id="download-csv-btn" class="cts-btn-general cts-btn-primary btn-push">
249
+ Descargar datos CSV en zip
250
+ </button>
251
+ <button id="download-xls-btn" class="cts-btn-general cts-btn-primary btn-push">
252
+ Descargar datos en XLSX
253
+ </button>
254
+ </div>
255
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
256
+ <script src="{% static 'js/download-table-xlsx.js' %}"></script>
257
+ {% else %}
258
+ {% include "../components/error-message.html" with message='Sin datos por mostrar aún' %}
259
+ {% endif %}
260
  {% endif %}
261
  </section>
262
  </article>
 
270
  {% endblock %}
271
 
272
  {% block extra_js %}
273
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js" crossorigin="anonymous"
274
+ referrerpolicy="no-referrer"></script>
275
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js" crossorigin="anonymous"
276
+ referrerpolicy="no-referrer"></script>
277
  <script src="{% static 'js/download-table-zip.js' %}"></script>
278
  <script src="{% static 'js/details-session.js' %}"></script>
279
  <script src="{% static 'js/showHiddenElement.js' %}"></script>
tecnicas/templatetags/__init__.py ADDED
File without changes
tecnicas/templatetags/custom_filters.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from django import template
2
+
3
+ register = template.Library()
4
+
5
+ @register.filter
6
+ def get_item(dictionary, key):
7
+ return dictionary.get(key)
tecnicas/views/apis/api_list_words_pf.py CHANGED
@@ -10,11 +10,18 @@ def apiListWordsPF(req: HttpRequest):
10
  elif req.method == "POST":
11
  try:
12
  data = json.loads(req.body.decode("utf-8"))
13
- raw_words = data.get("words", [])
14
  phase = data.get("phase", [])
 
 
 
 
 
 
 
 
 
 
15
 
16
- response = RatingPFListController.saveList(
17
- request=req, current_phase=phase, words=raw_words)
18
  return response
19
  except Exception as e:
20
  print("Error:", e)
 
10
  elif req.method == "POST":
11
  try:
12
  data = json.loads(req.body.decode("utf-8"))
 
13
  phase = data.get("phase", [])
14
+ if phase == 1 or phase == 2:
15
+ raw_words = data.get("words", [])
16
+ response = RatingPFListController.saveList(
17
+ request=req, current_phase=phase, words=raw_words)
18
+
19
+ elif phase >= 3:
20
+ word = data.get("word", [])
21
+ raw_data = data.get("data", [])
22
+ response = RatingPFListController.saveRatings(
23
+ request=req, word_rating=word, data=raw_data)
24
 
 
 
25
  return response
26
  except Exception as e:
27
  print("Error:", e)
tecnicas/views/sessions_management/session_details.py CHANGED
@@ -62,13 +62,24 @@ def sessionDetails(req: HttpRequest, session_code: str):
62
  reverse("cata_system:panel_sesiones", kwargs={"page": 1}))
63
 
64
  else:
65
- response = controller_view.getResponse(
66
  error="No se reconoce la acción a realizar")
67
 
68
  elif use_techinique == "perfil flash":
69
  controller_view = DetallesPFController(session=sensorial_session)
70
- response = controller_view.startRepetition(
71
- presenter=req.user.user_presentador, request=req)
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  else:
74
  response = noValidTechnique()
 
62
  reverse("cata_system:panel_sesiones", kwargs={"page": 1}))
63
 
64
  else:
65
+ response = controller_view.controllGetResponse(
66
  error="No se reconoce la acción a realizar")
67
 
68
  elif use_techinique == "perfil flash":
69
  controller_view = DetallesPFController(session=sensorial_session)
70
+
71
+ if req.POST["action"] == "start_session":
72
+ response = controller_view.startRepetition(
73
+ presenter=req.user.user_presentador, request=req)
74
+
75
+ elif req.POST.get("action") == "delete_session":
76
+ controller_view.deleteSesorialSession()
77
+ response = redirect(
78
+ reverse("cata_system:panel_sesiones", kwargs={"page": 1}))
79
+
80
+ else:
81
+ response = controller_view.controllGetResponse(
82
+ error="No se reconoce la acción a realizar")
83
 
84
  else:
85
  response = noValidTechnique()