chartManD commited on
Commit
bf9adb3
·
1 Parent(s): 136cbe4

Guardado de datos napping con sort

Browse files
tecnicas/controllers/api_controller/rating_napping_controller.py CHANGED
@@ -1,35 +1,237 @@
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, Palabra
5
  from tecnicas.forms import ListWordsForm
6
 
7
 
8
  class RatingNappingController:
9
  @staticmethod
10
- def saveRatingCoordinates(request: HttpRequest, data: list):
11
  participation = Participacion.objects.get(
12
  id=request.session["id_participation"]
13
  )
14
 
15
- print(data)
 
 
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  try:
18
  with transaction.atomic():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  products_map = RatingNappingController.getProductsMap(
20
  participation.tecnica)
21
 
 
22
  existing_ratings_map = RatingNappingController.getExistingRatingsMap(
23
- participation.tecnica, participation.catador
24
- )
25
 
26
- validation_result = RatingNappingController.validateWords(data)
27
- if validation_result is not None:
28
- return validation_result
 
 
29
 
 
30
  new_ratings = []
31
  ids_products = products_map.keys()
32
- for item in data:
33
  product_id = int(item["idProduct"])
34
  if product_id not in existing_ratings_map and product_id in ids_products:
35
  new_ratings.append(
@@ -44,16 +246,16 @@ class RatingNappingController:
44
  if new_ratings:
45
  Calificacion.objects.bulk_create(new_ratings)
46
  existing_ratings_map = RatingNappingController.getExistingRatingsMap(
47
- participation.tecnica, participation.catador
48
- )
49
 
 
50
  existing_points_map = RatingNappingController.getExistingPointsMap(
51
  existing_ratings_map.values())
52
 
53
  points_to_create = []
54
  points_to_update = []
55
 
56
- for item in data:
57
  product_id = int(item["idProduct"])
58
  rating = existing_ratings_map.get(product_id)
59
 
@@ -78,20 +280,14 @@ class RatingNappingController:
78
  if points_to_update:
79
  DatoPunto.objects.bulk_update(points_to_update, ['x', 'y'])
80
 
81
- RatingNappingController.processWordsForRatings(
82
- data, existing_ratings_map
83
- )
84
-
85
- return JsonResponse({"message": "Datos guardados exitosamente"})
86
 
87
  except Exception as e:
88
- print("ERROR:", e)
89
- return JsonResponse({"error": "Error al procesar datos"})
90
 
91
  @staticmethod
92
  def validateWords(data: list):
93
- """Valida todas las palabras de todos los items usando ListWordsForm.
94
- Retorna JsonResponse con error si hay problemas, None si todo está bien."""
95
  for item in data:
96
  words = item.get("words", [])
97
  if words:
@@ -109,9 +305,6 @@ class RatingNappingController:
109
 
110
  @staticmethod
111
  def processWordsForRatings(data: list, existing_ratings_map: dict):
112
- """Procesa y asocia palabras a las calificaciones.
113
- Crea palabras que no existan y las asocia a las calificaciones correspondientes."""
114
-
115
  all_words = set()
116
  for item in data:
117
  words = item.get("words", [])
 
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, Palabra, GrupoProducto, TecnicaModalidad
5
  from tecnicas.forms import ListWordsForm
6
 
7
 
8
  class RatingNappingController:
9
  @staticmethod
10
+ def saveRatingCoordinates(request: HttpRequest, data: list | dict):
11
  participation = Participacion.objects.get(
12
  id=request.session["id_participation"]
13
  )
14
 
15
+ name_mod = TecnicaModalidad.objects.get(
16
+ tecnica=participation.tecnica
17
+ ).modalidad.nombre.lower()
18
 
19
+ # Branch based on modality
20
+ if name_mod == 'sorting':
21
+ print(data)
22
+ return RatingNappingController.processSortingMode(
23
+ data, participation
24
+ )
25
+
26
+ if name_mod in ['sin modalidad', 'perfil ultra flash']:
27
+ return RatingNappingController.processNappOrPUF(
28
+ data, participation
29
+ )
30
+
31
+ else:
32
+ return JsonResponse({"error": "Modalidad no soportada"})
33
+
34
+ @staticmethod
35
+ def processSortingMode(data: dict, participation):
36
+ try:
37
+ with transaction.atomic():
38
+ # Extract products array (always present)
39
+ products = data.get("products", [])
40
+ if not products:
41
+ return JsonResponse({"error": "No se proporcionaron productos"})
42
+
43
+ existing_ratings_map, products_map = RatingNappingController.savePoints(
44
+ isSorting=True, products=products, participation=participation
45
+ )
46
+
47
+ # Process groups if they exist
48
+ groups = data.get("groups", {})
49
+ if groups:
50
+ RatingNappingController.processGroupsForSorting(
51
+ products, groups, participation, existing_ratings_map, products_map
52
+ )
53
+
54
+ return JsonResponse({"message": "Datos guardados exitosamente"})
55
+
56
+ except Exception as e:
57
+ print("ERROR:", e)
58
+ import traceback
59
+ traceback.print_exc()
60
+ return JsonResponse({"error": f"Error al procesar datos: {str(e)}"})
61
+
62
+ @staticmethod
63
+ def processNappOrPUF(data: list, participation):
64
  try:
65
  with transaction.atomic():
66
+ existing_ratings_map, products_map = RatingNappingController.savePoints(
67
+ isSorting=False, products=data, participation=participation
68
+ )
69
+
70
+ RatingNappingController.processWordsForRatings(
71
+ data, existing_ratings_map
72
+ )
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 processGroupsForSorting(products, groups, participation, existing_ratings_map, products_map):
82
+ """Process groups for sorting mode
83
+ - Creates/updates GrupoProducto instances
84
+ - Ensures products don't belong to multiple groups
85
+ - Associates words with groups
86
+ """
87
+ # Build mapping of product_id to group_id from products array
88
+ product_to_group = {}
89
+ for product_item in products:
90
+ group_code = product_item.get("group", "")
91
+ if group_code: # Only if product has a group assigned
92
+ product_id = int(product_item["idProduct"])
93
+ if product_id in product_to_group:
94
+ raise ValueError(
95
+ f"Producto {product_id} pertenece a múltiples grupos")
96
+ product_to_group[product_id] = group_code
97
+
98
+ # Get existing groups for this catador and technique
99
+ existing_groups = GrupoProducto.objects.filter(
100
+ tecnica=participation.tecnica,
101
+ catador=participation.catador
102
+ )
103
+
104
+ # Create a map of existing groups by their product composition
105
+ # We'll identify groups by the set of products they contain
106
+ existing_groups_map = {}
107
+ for group in existing_groups:
108
+ product_ids = set(group.productos.values_list('id', flat=True))
109
+ key = frozenset(product_ids)
110
+ existing_groups_map[key] = group
111
+
112
+ # Build new groups structure
113
+ groups_to_create = []
114
+ groups_to_update = []
115
+ group_products_map = {} # group_id -> [product_ids]
116
+
117
+ # Organize products by group
118
+ for product_id, group_code in product_to_group.items():
119
+ if group_code not in group_products_map:
120
+ group_products_map[group_code] = []
121
+ group_products_map[group_code].append(product_id)
122
+
123
+ # Process each group
124
+ for group_code, product_ids in group_products_map.items():
125
+ product_set = frozenset(product_ids)
126
+
127
+ # Check if this group already exists
128
+ if product_set in existing_groups_map:
129
+ group = existing_groups_map[product_set]
130
+ groups_to_update.append((group, group_code))
131
+ else:
132
+ # Create new group
133
+ group = GrupoProducto(
134
+ tecnica=participation.tecnica,
135
+ catador=participation.catador
136
+ )
137
+ groups_to_create.append((group, product_ids, group_code))
138
+
139
+ # Create new groups
140
+ created_groups = []
141
+ for group, product_ids, group_code in groups_to_create:
142
+ group.save() # Save first to get ID for M2M
143
+
144
+ # Add products to group
145
+ productos = [products_map[pid]
146
+ for pid in product_ids if pid in products_map]
147
+ group.productos.set(productos)
148
+
149
+ created_groups.append((group, group_code))
150
+
151
+ # Combine created and existing groups for word processing
152
+ all_groups_for_words = created_groups + groups_to_update
153
+
154
+ # Delete groups that no longer exist
155
+ current_group_sets = set(frozenset(pids)
156
+ for pids in group_products_map.values())
157
+ for product_set, group in existing_groups_map.items():
158
+ if product_set not in current_group_sets:
159
+ group.delete()
160
+
161
+ # Process words for groups
162
+ if groups:
163
+ RatingNappingController.processWordsForGroups(
164
+ groups, all_groups_for_words
165
+ )
166
+
167
+ @staticmethod
168
+ def processWordsForGroups(groups_data, groups_list):
169
+ """Process and associate words to groups
170
+ - Creates words that don't exist
171
+ - Associates words to GrupoProducto instances
172
+ - Handles concurrency
173
+ """
174
+ # Collect all unique words from all groups
175
+ all_words = set()
176
+ for group_id, words in groups_data.items():
177
+ if words:
178
+ all_words.update(words)
179
+
180
+ if not all_words:
181
+ # No words to process, just clear existing words from groups
182
+ for grupo, _ in groups_list:
183
+ grupo.palabras.clear()
184
+ return
185
+
186
+ # Get existing words
187
+ existing_words = Palabra.objects.filter(
188
+ nombre_palabra__in=all_words
189
+ )
190
+ existing_words_map = {w.nombre_palabra: w for w in existing_words}
191
+
192
+ # Create missing words with concurrency handling
193
+ word_objects = {}
194
+ for word_name in all_words:
195
+ if word_name in existing_words_map:
196
+ word_objects[word_name] = existing_words_map[word_name]
197
+ else:
198
+ word_obj, created = Palabra.objects.get_or_create(
199
+ nombre_palabra=word_name
200
+ )
201
+ word_objects[word_name] = word_obj
202
+
203
+ # Associate words with groups
204
+ for grupo, group_id in groups_list:
205
+ words = groups_data.get(group_id, [])
206
+ if words:
207
+ words_to_set = [word_objects[word_name]
208
+ for word_name in words if word_name in word_objects]
209
+ grupo.palabras.set(words_to_set)
210
+ else:
211
+ grupo.palabras.clear()
212
+
213
+ @staticmethod
214
+ def savePoints(isSorting: bool, products, participation):
215
+ try:
216
+ with transaction.atomic():
217
+ # Get products map for validation
218
  products_map = RatingNappingController.getProductsMap(
219
  participation.tecnica)
220
 
221
+ # Get existing ratings map
222
  existing_ratings_map = RatingNappingController.getExistingRatingsMap(
223
+ participation.tecnica, participation.catador)
 
224
 
225
+ if not isSorting:
226
+ validation_result = RatingNappingController.validateWords(
227
+ products)
228
+ if validation_result is not None:
229
+ return validation_result
230
 
231
+ # Create new ratings for products that don't have them
232
  new_ratings = []
233
  ids_products = products_map.keys()
234
+ for item in products:
235
  product_id = int(item["idProduct"])
236
  if product_id not in existing_ratings_map and product_id in ids_products:
237
  new_ratings.append(
 
246
  if new_ratings:
247
  Calificacion.objects.bulk_create(new_ratings)
248
  existing_ratings_map = RatingNappingController.getExistingRatingsMap(
249
+ participation.tecnica, participation.catador)
 
250
 
251
+ # Process DatoPunto instances from products array
252
  existing_points_map = RatingNappingController.getExistingPointsMap(
253
  existing_ratings_map.values())
254
 
255
  points_to_create = []
256
  points_to_update = []
257
 
258
+ for item in products:
259
  product_id = int(item["idProduct"])
260
  rating = existing_ratings_map.get(product_id)
261
 
 
280
  if points_to_update:
281
  DatoPunto.objects.bulk_update(points_to_update, ['x', 'y'])
282
 
283
+ return (existing_ratings_map, products_map)
 
 
 
 
284
 
285
  except Exception as e:
286
+ print(e)
287
+ return JsonResponse({"error": "Error al guardar los puntos"})
288
 
289
  @staticmethod
290
  def validateWords(data: list):
 
 
291
  for item in data:
292
  words = item.get("words", [])
293
  if words:
 
305
 
306
  @staticmethod
307
  def processWordsForRatings(data: list, existing_ratings_map: dict):
 
 
 
308
  all_words = set()
309
  for item in data:
310
  words = item.get("words", [])
tecnicas/static/js/test-napping-sort.js CHANGED
@@ -98,7 +98,7 @@ function initSortMode() {
98
  spanNotifaction("Fase de agrupación: Selecciona puntos y crea grupos.", false);
99
 
100
  // Auto-save points when transitioning to Phase 2
101
- window.saveData(false);
102
 
103
  // Enable point selection
104
  enablePointSelection();
@@ -285,7 +285,6 @@ function initSortMode() {
285
  const icon = iconsDisolveGroup.item(index);
286
  icon.remove();
287
  }
288
-
289
 
290
  spanNotifaction("Fase de descripción: Haz clic en un grupo para agregar palabras.", false);
291
 
@@ -423,16 +422,101 @@ function initSortMode() {
423
  return true;
424
  };
425
 
426
- // Data extension callback - adds groups and words to save data
427
- window.getExtraDataForSave = function (code) {
428
- const groupId = productToGroup[code];
429
- const group = groupId ? {
430
- groupId: groupId,
431
- groupWords: groupWords[groupId] || []
432
- } : null;
433
-
434
- return {
435
- group: group
436
- };
437
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  }
 
98
  spanNotifaction("Fase de agrupación: Selecciona puntos y crea grupos.", false);
99
 
100
  // Auto-save points when transitioning to Phase 2
101
+ sortModeSaveData(false);
102
 
103
  // Enable point selection
104
  enablePointSelection();
 
285
  const icon = iconsDisolveGroup.item(index);
286
  icon.remove();
287
  }
 
288
 
289
  spanNotifaction("Fase de descripción: Haz clic en un grupo para agregar palabras.", false);
290
 
 
422
  return true;
423
  };
424
 
425
+ // Override save-progress button to use sort mode save function
426
+ const saveProgressBtn = document.getElementById('save-progress');
427
+ // Remove existing event listener by cloning and replacing
428
+ const newSaveProgressBtn = saveProgressBtn.cloneNode(true);
429
+ newSaveProgressBtn.textContent = 'Guardar Progreso Sort';
430
+ saveProgressBtn.parentNode.replaceChild(newSaveProgressBtn, saveProgressBtn);
431
+
432
+ newSaveProgressBtn.addEventListener('click', async () => {
433
+ await sortModeSaveData(false);
434
+ });
435
+
436
+ // Override finish-session button to use sort mode save function
437
+ document.getElementById('finish-session').addEventListener('click', async (e) => {
438
+ e.preventDefault();
439
+ e.stopPropagation();
440
+ const success = await sortModeSaveData(true);
441
+ if (success) {
442
+ const formFinish = document.getElementById("form-finish-session");
443
+ formFinish.action = "";
444
+ formFinish.submit();
445
+ }
446
+ });
447
+
448
+ // Sort mode specific save function
449
+ async function sortModeSaveData(isFinishSession = false) {
450
+ // Run validation callback if it exists
451
+ if (window.beforeSaveData && typeof window.beforeSaveData === 'function') {
452
+ const validationResult = window.beforeSaveData(isFinishSession);
453
+ if (validationResult === false) {
454
+ return false;
455
+ }
456
+ }
457
+
458
+ // Build products array with basic position info and group assignment
459
+ const products = [];
460
+ for (const [code, point] of Object.entries(window.placedPoints)) {
461
+ const groupId = productToGroup[code];
462
+ products.push({
463
+ code: code,
464
+ x: point.x,
465
+ y: point.y,
466
+ idProduct: point.id,
467
+ group: groupId || "" // Empty string if no group assigned
468
+ });
469
+ }
470
+
471
+ // Build groups object with word arrays
472
+ const groups = {};
473
+ const groupIds = Object.keys(productGroups);
474
+
475
+ // Only include groups if they exist
476
+ if (groupIds.length > 0) {
477
+ for (const groupId of groupIds) {
478
+ groups[groupId] = groupWords[groupId] || [];
479
+ }
480
+ }
481
+
482
+ // Build the data structure
483
+ const data = { products: products };
484
+
485
+ // Only add groups if they exist
486
+ if (Object.keys(groups).length > 0) {
487
+ data.groups = groups;
488
+ }
489
+
490
+ const URL = "/cata/testers/api/rating-napping";
491
+ const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
492
+
493
+ try {
494
+ const response = await fetch(URL, {
495
+ method: "POST",
496
+ headers: {
497
+ "Content-Type": "application/json",
498
+ "X-CSRFToken": csrfToken,
499
+ },
500
+ body: JSON.stringify(data),
501
+ });
502
+
503
+ if (!response.ok) {
504
+ spanNotifaction("Error en la respuesta del servidor");
505
+ return false;
506
+ }
507
+
508
+ const result = await response.json();
509
+
510
+ if (result.error) {
511
+ spanNotifaction(result.error);
512
+ return false;
513
+ } else {
514
+ spanNotifaction(result.message, false);
515
+ return true;
516
+ }
517
+ } catch (error) {
518
+ spanNotifaction("Error en proceso de guardar los datos");
519
+ return false;
520
+ }
521
+ }
522
  }