chartManD commited on
Commit
3dc7a4b
·
1 Parent(s): f0a4ecd

Implementacion de combinacion de datos para napping sin modalidad con datos de cata, rata o escalas

Browse files
tecnicas/controllers/models_controller/dato_controller.py CHANGED
@@ -36,18 +36,18 @@ class DatoController():
36
  return controller_error(e.message)
37
 
38
  def setValue(self):
39
- if isinstance(self.value_rating, bool):
 
40
  self.value_data = ValorBooleano(valor=self.value_rating)
41
 
42
  else:
43
  type_scale = self.data.id_calificacion.id_tecnica.escala_tecnica.id_tipo_escala.nombre_escala
44
-
45
 
46
  if type_scale == "continua":
47
  decimal_value = self.value_rating/100
48
  value_rounded = round(decimal_value)
49
  self.value_data = ValorDecimal(valor=value_rounded)
50
-
51
  else:
52
  self.value_data = ValorDecimal(valor=self.value_rating)
53
 
 
36
  return controller_error(e.message)
37
 
38
  def setValue(self):
39
+ type_technique = self.data.id_calificacion.id_tecnica.tipo_tecnica
40
+ if type_technique == "cata":
41
  self.value_data = ValorBooleano(valor=self.value_rating)
42
 
43
  else:
44
  type_scale = self.data.id_calificacion.id_tecnica.escala_tecnica.id_tipo_escala.nombre_escala
 
45
 
46
  if type_scale == "continua":
47
  decimal_value = self.value_rating/100
48
  value_rounded = round(decimal_value)
49
  self.value_data = ValorDecimal(valor=value_rounded)
50
+
51
  else:
52
  self.value_data = ValorDecimal(valor=self.value_rating)
53
 
tecnicas/controllers/views_controller/session_management/details/details_controller.py CHANGED
@@ -14,6 +14,7 @@ class DetallesController():
14
 
15
  def controllGetResponse(self, request: HttpRequest, error: str = "", message: str = ""):
16
  context = self.getContext()
 
17
 
18
  if error != "" or error:
19
  context["error"] = error
 
14
 
15
  def controllGetResponse(self, request: HttpRequest, error: str = "", message: str = ""):
16
  context = self.getContext()
17
+ print(context)
18
 
19
  if error != "" or error:
20
  context["error"] = error
tecnicas/controllers/views_controller/session_management/details/details_napping_controller.py CHANGED
@@ -3,7 +3,11 @@ from django.shortcuts import redirect
3
  from django.urls import reverse
4
  from django.db.models import F
5
  from .details_controller import DetallesController
6
- from tecnicas.models import SesionSensorial, Presentador, Modalidad, TecnicaModalidad, Catador, Participacion, DatoPunto, Calificacion, GrupoProducto
 
 
 
 
7
  from tecnicas.utils import defaultdict_to_dict
8
  from collections import defaultdict
9
 
@@ -13,11 +17,10 @@ class DetallesNappingController(DetallesController):
13
  super().__init__(session)
14
  self.url_template = "tecnicas/manage_sesions/details-session-napping.html"
15
  self.url_next = "cata_system:monitor_sesion"
 
16
 
17
  def getContext(self):
18
- self.context = {
19
- "session": self.session,
20
- }
21
 
22
  self.defineStatus()
23
  self.setIsEndSession()
@@ -51,6 +54,9 @@ class DetallesNappingController(DetallesController):
51
  elif action == "start_sorting":
52
  response = self.startNapping(request=request)
53
 
 
 
 
54
  elif action == "delete_session":
55
  self.deleteSesorialSession()
56
  response = redirect(
@@ -225,3 +231,279 @@ class DetallesNappingController(DetallesController):
225
  elif not self.session.activo and self.session.tecnica.repeticion >= 1:
226
  self.context["finished"] = True
227
  return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from django.urls import reverse
4
  from django.db.models import F
5
  from .details_controller import DetallesController
6
+ from tecnicas.models import (
7
+ SesionSensorial, Presentador, Modalidad, TecnicaModalidad, Catador,
8
+ Participacion, DatoPunto, Calificacion, GrupoProducto, ValorBooleano,
9
+ ValorDecimal, Producto, Escala, EsVocabulario, Vocabulario
10
+ )
11
  from tecnicas.utils import defaultdict_to_dict
12
  from collections import defaultdict
13
 
 
17
  super().__init__(session)
18
  self.url_template = "tecnicas/manage_sesions/details-session-napping.html"
19
  self.url_next = "cata_system:monitor_sesion"
20
+ self.context = {}
21
 
22
  def getContext(self):
23
+ self.context["session"] = self.session
 
 
24
 
25
  self.defineStatus()
26
  self.setIsEndSession()
 
54
  elif action == "start_sorting":
55
  response = self.startNapping(request=request)
56
 
57
+ elif action == "combine_sessions":
58
+ response = self.combineSessions(request=request)
59
+
60
  elif action == "delete_session":
61
  self.deleteSesorialSession()
62
  response = redirect(
 
231
  elif not self.session.activo and self.session.tecnica.repeticion >= 1:
232
  self.context["finished"] = True
233
  return
234
+
235
+ # ==================== SESSION COMBINATION METHODS ====================
236
+
237
+ def combineSessions(self, request: HttpRequest):
238
+ """Handle session combination request"""
239
+ session_b_code = request.POST.get("session_b_code", "").strip()
240
+
241
+ if not session_b_code:
242
+ return self.controllGetResponse(
243
+ error="Debe proporcionar un código de sesión", request=request)
244
+
245
+ # Validate and get session B
246
+ validation_result = self.validateSessionCombination(session_b_code)
247
+
248
+ if validation_result.get("error"):
249
+ return self.controllGetResponse(
250
+ error=validation_result["error"], request=request)
251
+
252
+ session_b = validation_result["session_b"]
253
+ technique_type = validation_result["technique_type"]
254
+
255
+ # Get combined data based on technique type
256
+ if technique_type == "cata":
257
+ combined_data = self.getCombinedDataForCATA(session_b)
258
+ elif technique_type == "rata":
259
+ combined_data = self.getCombinedDataForRATA(session_b)
260
+ elif technique_type == "escalas":
261
+ combined_data = self.getCombinedDataForEscalas(session_b)
262
+ else:
263
+ return self.controllGetResponse(
264
+ error="Tipo de técnica no soportado para combinación", request=request)
265
+
266
+ # Add combined data to context
267
+ self.context["combined_data"] = combined_data
268
+ self.context["session_b"] = session_b
269
+ self.context["session_b_technique_type"] = technique_type
270
+
271
+ return self.controllGetResponse(request=request)
272
+
273
+ def validateSessionCombination(self, session_b_code: str):
274
+ """Validate that Session B can be combined with Session A (Napping)"""
275
+ result = {"error": None, "session_b": None, "technique_type": None}
276
+
277
+ # Check if Session B exists
278
+ try:
279
+ session_b = SesionSensorial.objects.select_related(
280
+ "tecnica__tipo_tecnica").get(codigo_sesion=session_b_code)
281
+ except SesionSensorial.DoesNotExist:
282
+ result["error"] = f"No existe una sesión con el código: {session_b_code}"
283
+ return result
284
+
285
+ # Check if Session B technique is CATA, RATA, or Escalas
286
+ technique_type = session_b.tecnica.tipo_tecnica.nombre_tecnica
287
+ valid_techniques = ["cata", "rata", "escalas"]
288
+
289
+ if technique_type not in valid_techniques:
290
+ result[
291
+ "error"] = f"La sesión B debe usar CATA, RATA o Escalas. Técnica actual: {technique_type}"
292
+ return result
293
+
294
+ # Check if Session B is finished
295
+ if session_b.activo:
296
+ result["error"] = "La sesión B debe estar finalizada (no activa)"
297
+ return result
298
+
299
+ if session_b.tecnica.repeticion < 1:
300
+ result["error"] = "La sesión B debe haber completado al menos una repetición"
301
+ return result
302
+
303
+ # Get products from both sessions
304
+ products_a = set(
305
+ Producto.objects.filter(
306
+ calificacion_producto__id_tecnica=self.session.tecnica
307
+ ).values_list("codigoProducto", flat=True).distinct()
308
+ )
309
+
310
+ products_b = set(
311
+ Producto.objects.filter(
312
+ calificacion_producto__id_tecnica=session_b.tecnica
313
+ ).values_list("codigoProducto", flat=True).distinct()
314
+ )
315
+
316
+ # Check if products match
317
+ if products_a != products_b:
318
+ result["error"] = f"Los productos no coinciden. Sesión A: {len(products_a)} productos, Sesión B: {len(products_b)} productos"
319
+ return result
320
+
321
+ # Get tasters from both sessions
322
+ tasters_a = set(
323
+ Participacion.objects.filter(
324
+ tecnica=self.session.tecnica
325
+ ).values_list("catador__user__username", flat=True)
326
+ )
327
+
328
+ tasters_b = set(
329
+ Participacion.objects.filter(
330
+ tecnica=session_b.tecnica
331
+ ).values_list("catador__user__username", flat=True)
332
+ )
333
+
334
+ # Check if tasters match
335
+ if tasters_a != tasters_b:
336
+ result["error"] = f"Los catadores no coinciden. Sesión A: {len(tasters_a)} catadores, Sesión B: {len(tasters_b)} catadores"
337
+ return result
338
+
339
+ result["session_b"] = session_b
340
+ result["technique_type"] = technique_type
341
+ return result
342
+
343
+ def getCombinedDataForCATA(self, session_b: SesionSensorial):
344
+ """Get combined data for CATA technique (word frequencies)"""
345
+ from collections import Counter
346
+
347
+ # Get all ratings for session B
348
+ ratings_b = Calificacion.objects.filter(id_tecnica=session_b.tecnica)
349
+
350
+ # Get boolean values (CATA uses boolean)
351
+ data = (
352
+ ValorBooleano.objects
353
+ .filter(id_dato__id_calificacion__in=ratings_b, valor=True)
354
+ .values(
355
+ palabra=F("id_dato__id_palabra__nombre_palabra"),
356
+ producto=F(
357
+ "id_dato__id_calificacion__id_producto__codigoProducto"),
358
+ )
359
+ )
360
+
361
+ # Count word frequencies per product
362
+ word_frequencies = defaultdict(Counter)
363
+ all_words_set = set()
364
+
365
+ for item in data:
366
+ palabra = item["palabra"]
367
+ producto = item["producto"]
368
+ word_frequencies[producto][palabra] += 1
369
+ all_words_set.add(palabra)
370
+
371
+ # Get vocabulary info if exists
372
+ vocabulary_info = self.getVocabularyInfo(session_b.tecnica)
373
+
374
+ return {
375
+ "word_frequencies": defaultdict_to_dict(word_frequencies),
376
+ "all_words": sorted(all_words_set),
377
+ "vocabulary_info": vocabulary_info,
378
+ }
379
+
380
+ def getCombinedDataForRATA(self, session_b: SesionSensorial):
381
+ """Get combined data for RATA technique (word averages)"""
382
+
383
+ # Get all ratings for session B
384
+ ratings_b = Calificacion.objects.filter(id_tecnica=session_b.tecnica)
385
+
386
+ # Get decimal values (RATA uses decimal)
387
+ data = (
388
+ ValorDecimal.objects
389
+ .filter(id_dato__id_calificacion__in=ratings_b)
390
+ .values(
391
+ palabra=F("id_dato__id_palabra__nombre_palabra"),
392
+ producto=F(
393
+ "id_dato__id_calificacion__id_producto__codigoProducto"),
394
+ valor_decimal=F("valor"),
395
+ )
396
+ )
397
+
398
+ # Calculate averages per product per word
399
+ word_sums = defaultdict(lambda: defaultdict(list))
400
+ all_words_set = set()
401
+
402
+ for item in data:
403
+ palabra = item["palabra"]
404
+ producto = item["producto"]
405
+ valor = item["valor_decimal"]
406
+ word_sums[producto][palabra].append(valor)
407
+ all_words_set.add(palabra)
408
+
409
+ # Calculate averages
410
+ word_averages = {}
411
+ for producto, palabras in word_sums.items():
412
+ word_averages[producto] = {}
413
+ for palabra, valores in palabras.items():
414
+ word_averages[producto][palabra] = sum(valores) / len(valores)
415
+
416
+ # Get vocabulary info if exists
417
+ vocabulary_info = self.getVocabularyInfo(session_b.tecnica)
418
+
419
+ return {
420
+ "word_averages": word_averages,
421
+ "all_words": sorted(all_words_set),
422
+ "vocabulary_info": vocabulary_info,
423
+ }
424
+
425
+ def getCombinedDataForEscalas(self, session_b: SesionSensorial):
426
+ """Get combined data for Escalas technique (averages across repetitions)"""
427
+
428
+ # Get all ratings for session B
429
+ ratings_b = Calificacion.objects.filter(id_tecnica=session_b.tecnica)
430
+
431
+ # Get decimal values grouped by repetition
432
+ data = (
433
+ ValorDecimal.objects
434
+ .filter(id_dato__id_calificacion__in=ratings_b)
435
+ .values(
436
+ palabra=F("id_dato__id_palabra__nombre_palabra"),
437
+ producto=F(
438
+ "id_dato__id_calificacion__id_producto__codigoProducto"),
439
+ repeticion=F("id_dato__id_calificacion__num_repeticion"),
440
+ valor_decimal=F("valor"),
441
+ )
442
+ )
443
+
444
+ # Calculate averages per repetition (like RATA), then average across repetitions
445
+ # Structure: {producto: {repeticion: {palabra: [valores]}}}
446
+ repetition_values = defaultdict(
447
+ lambda: defaultdict(lambda: defaultdict(list)))
448
+ all_words_set = set()
449
+
450
+ for item in data:
451
+ palabra = item["palabra"]
452
+ producto = item["producto"]
453
+ repeticion = item["repeticion"]
454
+ valor = item["valor_decimal"]
455
+ # Collect all values for averaging
456
+ repetition_values[producto][repeticion][palabra].append(valor)
457
+ all_words_set.add(palabra)
458
+
459
+ # Calculate average per repetition, then average across repetitions
460
+ word_averages = {}
461
+ for producto, repeticiones in repetition_values.items():
462
+ word_averages[producto] = {}
463
+ # Get all words for this product across all repetitions
464
+ all_product_words = set()
465
+ for rep_words in repeticiones.values():
466
+ all_product_words.update(rep_words.keys())
467
+
468
+ # Calculate average for each word
469
+ for palabra in all_product_words:
470
+ # Get average for each repetition
471
+ rep_averages = []
472
+ for rep in repeticiones.keys():
473
+ if palabra in repeticiones[rep]:
474
+ valores = repeticiones[rep][palabra]
475
+ rep_averages.append(sum(valores) / len(valores))
476
+
477
+ # Average the repetition averages
478
+ if rep_averages:
479
+ word_averages[producto][palabra] = sum(
480
+ rep_averages) / len(rep_averages)
481
+
482
+ # Get scale and vocabulary info
483
+ scale = Escala.objects.get(tecnica=session_b.tecnica)
484
+ scale_info = None
485
+ if scale:
486
+ scale_info = {
487
+ "type": scale.id_tipo_escala.nombre_escala,
488
+ "size": scale.longitud
489
+ }
490
+
491
+ vocabulary_info = self.getVocabularyInfo(session_b.tecnica)
492
+
493
+ return {
494
+ "word_averages": word_averages,
495
+ "all_words": sorted(all_words_set),
496
+ "scale_info": scale_info,
497
+ "vocabulary_info": vocabulary_info,
498
+ "num_repetitions": session_b.tecnica.repeticion,
499
+ }
500
+
501
+ def getVocabularyInfo(self, tecnica):
502
+ es_vocabulario = EsVocabulario.objects.filter(
503
+ id_tecnica=tecnica).first()
504
+ if es_vocabulario:
505
+ vocabulario = es_vocabulario.id_vocabulario
506
+ return {
507
+ "nombre": vocabulario.nombre_vocabulario,
508
+ }
509
+ return None
tecnicas/templates/tecnicas/components/table-napping-combined.html ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% load static %}
2
+ {% load custom_filters %}
3
+ <article class="space-y-4 text-black">
4
+ <!-- Session B Information -->
5
+ <div class="bg-surface-card p-4 rounded-lg space-y-2">
6
+ <h3 class="font-bold text-lg">Información de la Sesión B</h3>
7
+ <div class="grid grid-cols-2 max-sm:grid-cols-1 gap-2 text-sm">
8
+ <p><span class="font-semibold">Código:</span> {{ session_b.codigo_sesion }}</p>
9
+ <p><span class="font-semibold">Técnica:</span> {{ technique_type }}</p>
10
+ <p><span class="font-semibold">Nombre:</span> {{ session_b.nombre_sesion|default:"Sin nombre" }}</p>
11
+ <p><span class="font-semibold">Catadores:</span> {{ testers|length }}</p>
12
+
13
+ {% if combined_data.vocabulary_info %}
14
+ <p class="col-span-2 max-sm:col-span-1">
15
+ <span class="font-semibold">Vocabulario:</span> {{ combined_data.vocabulary_info.nombre }}
16
+ </p>
17
+ {% endif %}
18
+
19
+ {% if combined_data.scale_info %}
20
+ <p><span class="font-semibold">Escala:</span> {{ combined_data.scale_info.type }}</p>
21
+ <p><span class="font-semibold">Longitud:</span> {{ combined_data.scale_info.size }}</p>
22
+ {% endif %}
23
+
24
+ {% if combined_data.num_repetitions %}
25
+ <p><span class="font-semibold">Repeticiones:</span> {{ combined_data.num_repetitions }}</p>
26
+ {% endif %}
27
+
28
+ <p class="col-span-2 max-sm:col-span-1">
29
+ <span class="font-semibold">Palabras utilizadas:</span>
30
+ {{ combined_data.all_words|join:", " }}
31
+ </p>
32
+ </div>
33
+ </div>
34
+
35
+ <!-- Combined Data Table -->
36
+ <div class="overflow-x-auto rounded-lg border border-surface-general">
37
+ <table id="generic-donwload-table" class="min-w-max w-full text-sm text-center border-collapse">
38
+ <thead class="bg-surface-sweet font-semibold">
39
+ <tr>
40
+ <th class="py-2 px-3 border border-surface-general">Producto</th>
41
+ {% for tester in testers %}
42
+ <th class="py-2 px-3 border border-surface-general uppercase">
43
+ X{{ forloop.counter }}
44
+ </th>
45
+ <th class="py-2 px-3 border border-surface-general uppercase">
46
+ Y{{ forloop.counter }}
47
+ </th>
48
+ {% endfor %}
49
+ {% for word in combined_data.all_words %}
50
+ <th class="py-2 px-3 border border-surface-general capitalize">
51
+ {{ word }}
52
+ </th>
53
+ {% endfor %}
54
+ </tr>
55
+ </thead>
56
+ <tbody class="bg-surface-ligt divide-y divide-gray-200">
57
+ {% for product, coordinates_tester in coordinates_no_mode.items %}
58
+ <tr>
59
+ <td class="py-2 px-3 border border-surface-general">{{ product }}</td>
60
+ {% for tester in testers %}
61
+ {% with points=coordinates_tester|get_item:tester.user.username %}
62
+ <td class="py-2 px-3 border border-surface-general">
63
+ {{ points.px }}
64
+ </td>
65
+ <td class="py-2 px-3 border border-surface-general">
66
+ {{ points.py }}
67
+ </td>
68
+ {% endwith %}
69
+ {% endfor %}
70
+ {% for word in combined_data.all_words %}
71
+ <td class="py-2 px-3 border border-surface-general">
72
+ {% if combined_data.word_frequencies %}
73
+ {% with freq=combined_data.word_frequencies|get_item:product|get_item:word %}
74
+ {% if freq %}{{ freq }}{% else %}0{% endif %}
75
+ {% endwith %}
76
+ {% elif combined_data.word_averages %}
77
+ {% with avg=combined_data.word_averages|get_item:product|get_item:word %}
78
+ {% if avg %}{{ avg|floatformat:2 }}{% else %}0.00{% endif %}
79
+ {% endwith %}
80
+ {% else %}
81
+ 0
82
+ {% endif %}
83
+ </td>
84
+ {% endfor %}
85
+ </tr>
86
+ {% endfor %}
87
+ </tbody>
88
+ </table>
89
+ </div>
90
+
91
+ <div class="flex justify-end">
92
+ <button id="download-csv-btn" class="cts-btn-general cts-btn-primary btn-push"
93
+ data-session-name="{{ session.nombre_sesion }}_combinado_{{ session_b.codigo_sesion }}"
94
+ data-session-code="{{ session.codigo_sesion }}">
95
+ Descargar datos combinados en CSV
96
+ </button>
97
+ </div>
98
+ </article>
99
+
100
+ <script src="{% static 'js/download-table-csv.js' %}"></script>
tecnicas/templates/tecnicas/manage_sesions/details-session-napping.html CHANGED
@@ -203,6 +203,49 @@
203
  {% include "../components/error-message.html" with message='Sin datos por mostrar aún' %}
204
  {% endif %}
205
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
  <form action="" method="post" class="form-action-session hidden">
208
  <input type="hidden" name="action" class="action-option">
 
203
  {% include "../components/error-message.html" with message='Sin datos por mostrar aún' %}
204
  {% endif %}
205
 
206
+ {% if mode == "sin modalidad" and finished %}
207
+ <p class="text-black font-bold text-2xl border-b-2">
208
+ Combinar datos con otra sesión
209
+ </p>
210
+ <article class="space-y-4 text-black bg-surface-card p-4 rounded-2xl">
211
+ <p class="text-lg">
212
+ Puede combinar los datos de esta sesión de Napping con otra sesión que use CATA, RATA o Escalas.
213
+ Ambas sesiones deben tener los mismos productos y catadores
214
+ </p>
215
+
216
+ <form method="post" class="space-y-4">
217
+ {% csrf_token %}
218
+ <input type="hidden" name="action" value="combine_sessions">
219
+
220
+ <div class="flex flex-col gap-2">
221
+ <label for="session_b_code" class="font-semibold">
222
+ Código de la sesión a combinar:
223
+ </label>
224
+ <div class="flex gap-2 max-sm:flex-col">
225
+ <input
226
+ type="text"
227
+ id="session_b_code"
228
+ name="session_b_code"
229
+ class="flex-1 px-4 py-2 border border-surface-general rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
230
+ placeholder="Ingrese el código de sesión"
231
+ required
232
+ >
233
+ <button type="submit" class="cts-btn-general cts-btn-primary btn-push">
234
+ Validar y Combinar
235
+ </button>
236
+ </div>
237
+ </div>
238
+ </form>
239
+ </article>
240
+ {% endif %}
241
+
242
+ {% if combined_data %}
243
+ <p class="text-black font-bold text-2xl border-b-2 mt-8">
244
+ Datos Combinados (Napping + {{ session_b_technique_type }})
245
+ </p>
246
+ {% include "../components/table-napping-combined.html" with session=session session_b=session_b technique_type=session_b_technique_type combined_data=combined_data testers=testers coordinates_no_mode=coordinates_no_mode %}
247
+ {% endif %}
248
+
249
 
250
  <form action="" method="post" class="form-action-session hidden">
251
  <input type="hidden" name="action" class="action-option">