1(function($) { 2 'use strict'; 3 4 // --- Verificações de dependências --- 5 if (!window.Looper || !window.nunjucks || !window.Urls) { 6 console.warn("Dependências (Looper, Nunjucks, Urls) não encontradas. Evaluation não será executado."); 7 return; 8 } 9 10 // Flag de controle para evitar execuções simultâneas 11 var isProcessing = false; 12 13 // --- Constantes --- 14 var CONSTS = { 15 CONTAINER_SELECTOR: ".plus_rating_evaluation", 16 CRITERIA_BOX_CLASS: "plus_rating_evaluation_criteria", 17 REVIEW_BOX_CLASS: "plus_rating_evaluation_review", 18 MESSAGE_CLASS: "plus_rating_evaluation_message", 19 SUBMIT_BTN_CLASS: "plus_rating_evaluation_submit", 20 PROCESSED_ATTR: "data-processed", 21 MODEL_ATTR: "data-plus-rating-evaluation-model", 22 NODE_ID_ATTR: "data-plus-rating-evaluation-nodeid", 23 IS_AUTH_ATTR: "data-plus-rating-evaluation-is-authenticated", 24 API_KEY: "evaluation", 25 API_CONFIG_URL: "hidra-rest-api:plus_rating_evaluation_rest-config", 26 API_SUMMARY_LIGHT_URL: "hidra-rest-api:plus_rating_evaluation_rest-summary-light", 27 API_SUMMARY_URL: "hidra-rest-api:plus_rating_evaluation_rest-summary", 28 API_SUBMIT_URL: "hidra-rest-api:plus_rating_evaluation_rest-submit", 29 API_DELETE_SESSION_URL: "hidra-rest-api:plus_rating_evaluation_rest-delete-session", 30 API_TOGGLE_REVIEW_PUBLIC_URL: "hidra-rest-api:plus_rating_evaluation_rest-toggle-review-public", 31 API_EDIT_REVIEW_URL: "hidra-rest-api:plus_rating_evaluation_rest-edit-review", 32 API_EDIT_EVALUATION_URL: "hidra-rest-api:plus_rating_evaluation_rest-edit-evaluation", 33 TEMPLATE_LOADING: "plus_rating/nunjucks/plugins/evaluation/loading.njk", 34 TEMPLATE_BADGE: "plus_rating/nunjucks/plugins/evaluation/badge.njk", 35 TEMPLATE_INLINE: "plus_rating/nunjucks/plugins/evaluation/inline.njk", 36 VARIANT_ATTR: "data-plus-rating-evaluation-variant", 37 TEMPLATE_MODAL: "plus_rating/nunjucks/plugins/evaluation/modal.njk", 38 TEMPLATE_DONE: "plus_rating/nunjucks/plugins/evaluation/done.njk", 39 TEMPLATE_FAIL: "plus_rating/nunjucks/plugins/evaluation/fail.njk", 40 TEMPLATE_MSG_SUCCESS: "plus_rating/nunjucks/plugins/evaluation/message_success.njk", 41 TEMPLATE_MSG_ERROR: "plus_rating/nunjucks/plugins/evaluation/message_error.njk", 42 TEMPLATE_CONFIRM_MODAL: "plus_rating/nunjucks/plugins/evaluation/confirm_modal.njk", 43 TEMPLATE_MODAL_WRAPPER: "plus_rating/nunjucks/plugins/evaluation/modal_wrapper.njk", 44 MODAL_ID: "plus-rating-evaluation-modal", 45 CONFIRM_MODAL_ID: "plus-rating-evaluation-confirm-modal" 46 }; 47 48 // --- Verificações de URLs --- 49 var requiredUrls = [ 50 CONSTS.API_CONFIG_URL, CONSTS.API_SUMMARY_LIGHT_URL, CONSTS.API_SUMMARY_URL, 51 CONSTS.API_SUBMIT_URL, CONSTS.API_DELETE_SESSION_URL, 52 CONSTS.API_TOGGLE_REVIEW_PUBLIC_URL, CONSTS.API_EDIT_REVIEW_URL, 53 CONSTS.API_EDIT_EVALUATION_URL 54 ]; 55 for (var i = 0; i < requiredUrls.length; i++) { 56 if (typeof Urls[requiredUrls[i]] !== 'function') { 57 console.warn("Url do evaluation nao encontrada: " + requiredUrls[i]); 58 return; 59 } 60 } 61 62 // --- Modal de confirmação --- 63 function ensureConfirmModal() { 64 if ($('#' + CONSTS.CONFIRM_MODAL_ID).length > 0) return; 65 $('body').append(nunjucks.render(CONSTS.TEMPLATE_CONFIRM_MODAL, { 66 modal_id: CONSTS.CONFIRM_MODAL_ID 67 })); 68 } 69 70 function showConfirmModal(title, body, onConfirm) { 71 ensureConfirmModal(); 72 var $modal = $('#' + CONSTS.CONFIRM_MODAL_ID); 73 $modal.find('.modal-title').text(title); 74 $modal.find('.modal-body').text(body); 75 var $confirmBtn = $modal.find('.plus_rating_evaluation_confirm_btn'); 76 $confirmBtn.off('click').on('click', function() { 77 $modal.modal('hide'); 78 onConfirm(); 79 }); 80 $modal.modal({backdrop: 'static'}); 81 } 82 83 // --- Modal principal de avaliação --- 84 function ensureEvaluationModal() { 85 if ($('#' + CONSTS.MODAL_ID).length > 0) return; 86 $('body').append(nunjucks.render(CONSTS.TEMPLATE_MODAL_WRAPPER, { 87 modal_id: CONSTS.MODAL_ID 88 })); 89 } 90 91 // --- Configuração padrão do rater --- 92 var raterDefaultOptions = { 93 step_size: 1, 94 max_value: 5, 95 cursor: "pointer", 96 initial_value: 0, 97 readonly: false, 98 change_once: false, 99 symbols: { 100 star: { 101 base: "<i class='fal fa-lg text-rating fa-rating'></i>", 102 hover: "<i class='fa fa-lg text-rating-hover fa-rating'></i>", 103 selected: "<i class='fa fa-lg text-rating-active fa-rating'></i>" 104 } 105 }, 106 selected_symbol_type: "star" 107 }; 108 109 // Cache de configs por content_type 110 var configCache = {}; 111 112 /** 113 * Determina se o usuário pode avaliar (novo submit ou atualização). 114 * Anônimo nunca pode â backend rejeita mutação sem autenticação. 115 */ 116 function canUserEvaluate(config, summary, isAuthenticated) { 117 if (!isAuthenticated) return false;
118 if (!summary.user_scores || Object.keys(summary.user_scores).length === 0) return true; 119 if (config.allow_multiple) { 120 return summary.can_evaluate_again !== false; 121 } 122 return true; 123 } 124 125 // --- Helpers reutilizáveis --- 126 127 /** 128 * Coleta scores dos raters de critério dentro de um container. 129 * Retorna {scores, allRated} â scores é dict {criteria_id: value}. 130 */ 131 function collectScores($container, config, raterClassPrefix) { 132 var scores = {}; 133 var allRated = true; 134 config.criteria.forEach(function(criterion) { 135 var $rater = $container.find('.' + raterClassPrefix + criterion.id); 136 var value = $rater.rate("getValue"); 137 if (value > 0) { 138 scores[criterion.id] = value; 139 } else { 140 allRated = false; 141 } 142 }); 143 return { scores: scores, allRated: allRated }; 144 } 145 146 /** 147 * Extrai mensagem de erro de um xhr response do DRF. 148 */ 149 function getErrorMessage(xhr) { 150 if (xhr.responseJSON && xhr.responseJSON.detail) { 151 return xhr.responseJSON.detail; 152 } 153 return ''; 154 } 155 156 /** 157 * Busca os scores de uma sessão especÃfica dentro do summary. 158 */ 159 function getSessionScores(summary, sessionId) { 160 for (var i = 0; i < summary.user_sessions.length; i++) { 161 if (summary.user_sessions[i].session_id === sessionId && summary.user_sessions[i].scores) { 162 return summary.user_sessions[i].scores; 163 } 164 } 165 return summary.user_scores || {}; 166 } 167 168 /** 169 * Inicializa raters editáveis com scores de uma sessão. 170 */ 171 function initSessionRaters($container, config, sessionScores, raterClassPrefix) { 172 config.criteria.forEach(function(criterion) { 173 var $rater = $container.find('.' + raterClassPrefix + criterion.id); 174 var initialValue = sessionScores[criterion.id] || 0; 175 $rater.attr("data-rate-value", initialValue).rate($.extend(true, {}, raterDefaultOptions, { 176 max_value: config.max_stars 177 })); 178 }); 179 } 180 181 // ========================================================================= 182 // FASE 1 â Badge inline (summary-light) 183 // ========================================================================= 184 185 /** 186 * Renderiza o badge compacto dentro do container. 187 */ 188 function renderBadge($container, lightSummary) { 189 var variant = $container.attr(CONSTS.VARIANT_ATTR); 190 var template = variant === 'inline' ? CONSTS.TEMPLATE_INLINE : CONSTS.TEMPLATE_BADGE; 191 var isAuthenticated = parseInt($container.attr(CONSTS.IS_AUTH_ATTR), 10) === 1; 192 $container.addClass(Looper.template_nunjucks_initialized_class); 193 $container.html(nunjucks.render(template, { 194 avg_score: lightSummary.avg_score || 0, 195 avg_score_pct: lightSummary.avg_score_pct || 0, 196 total_evaluations: lightSummary.total_evaluations || 0, 197 criteria_scores: lightSummary.criteria_scores || [], 198 is_authenticated: isAuthenticated 199 })); 200 201 var badgeRaterOptions = $.extend(true, {}, raterDefaultOptions, { 202 max_value: lightSummary.max_stars || 5, 203 readonly: true, 204 step_size: 0.1 205 }); 206 207 // Inicializa o rater em readonly para cada critério 208 if (lightSummary.criteria_scores) { 209 lightSummary.criteria_scores.forEach(function(criterion, index) { 210 var $rater = $container.find('.plus_rating_badge_criterion_' + index); 211 if ($rater.length > 0) { 212 $rater.attr("data-rate-value", criterion.avg_score || 0).rate(badgeRaterOptions); 213 } 214 }); 215 } 216 217 // Inicializa o rater em readonly para a média geral no detalhe 218 var $overallRater = $container.find('.plus_rating_badge_overall'); 219 if ($overallRater.length > 0) { 220 $overallRater.attr("data-rate-value", lightSummary.avg_score || 0).rate(badgeRaterOptions); 221 } 222 223 // Inicializa o rater em readonly para a média geral no summary 224 var $overallMainRater = $container.find('.plus_rating_badge_overall_main'); 225 if ($overallMainRater.length > 0) { 226 $overallMainRater.attr("data-rate-value", lightSummary.avg_score || 0).rate(badgeRaterOptions); 227 } 228 229 // Handler: ao clicar no link, abre o modal com dados completos 230 $container.find('.plus_rating_evaluation_open_modal').on('click', function(e) { 231 e.preventDefault(); 232 openEvaluationModal($container); 233 }); 234 } 235 236 // ========================================================================= 237 // FASE 2 â Modal completo (summary + config sob demanda) 238 // ========================================================================= 239 240 /** 241 * Abre o modal e carrega dados completos do summary + config. 242 */ 243 function openEvaluationModal($container) { 244 ensureEvaluationModal();
245 var $modal = $('#' + CONSTS.MODAL_ID); 246 var contentType = $container.attr(CONSTS.MODEL_ATTR); 247 var objectId = $container.attr(CONSTS.NODE_ID_ATTR); 248 249 // Renderiza o modal com loading 250 $modal.html(nunjucks.render(CONSTS.TEMPLATE_MODAL, {})); 251 $modal.modal('show'); 252 253 var $modalBody = $modal.find('.plus_rating_evaluation_modal_body'); 254 255 // Fetch paralelo: summary completo + config 256 var summaryReq = $.ajax({ 257 url: Urls[CONSTS.API_SUMMARY_URL](), 258 data: { content_type: contentType, object_id: objectId }, 259 method: "GET", 260 dataType: "json" 261 }); 262 263 var configReq; 264 if (configCache[contentType]) { 265 configReq = $.Deferred().resolve(configCache[contentType]); 266 } else { 267 configReq = $.ajax({ 268 url: Urls[CONSTS.API_CONFIG_URL](), 269 data: { content_type: contentType }, 270 method: "GET", 271 dataType: "json" 272 }); 273 } 274 275 $.when(summaryReq, configReq).done(function(summaryResult, configResult) { 276 // $.when retorna arrays quando são ajax, valor direto quando deferred 277 var summaryData = Array.isArray(summaryResult) ? summaryResult[0] : summaryResult; 278 var configData = Array.isArray(configResult) ? configResult[0] : configResult; 279 280 configCache[contentType] = configData; 281 282 if (!configData.active) { 283 $modalBody.html(nunjucks.render(CONSTS.TEMPLATE_FAIL, {})); 284 return; 285 } 286 287 renderModalContent($modal, $container, configData, summaryData); 288 }).fail(function() { 289 $modalBody.html(nunjucks.render(CONSTS.TEMPLATE_FAIL, {})); 290 }); 291 } 292 293 /** 294 * Renderiza o conteúdo completo dentro do modal e liga os handlers. 295 */ 296 function renderModalContent($modal, $container, config, summary) { 297 var $modalBody = $modal.find('.plus_rating_evaluation_modal_body'); 298 var isAuthenticated = parseInt($container.attr(CONSTS.IS_AUTH_ATTR), 10) === 1; 299 var userCanEvaluate = canUserEvaluate(config, summary, isAuthenticated); 300 var widgetId = 'modal_eval_' + Math.random().toString(36).substr(2, 6); 301 var contentType = $container.attr(CONSTS.MODEL_ATTR); 302 var objectId = $container.attr(CONSTS.NODE_ID_ATTR); 303 304 var nunjucksContext = { 305 criteria_box_class: CONSTS.CRITERIA_BOX_CLASS, 306 review_box_class: CONSTS.REVIEW_BOX_CLASS, 307 message_class: CONSTS.MESSAGE_CLASS, 308 submit_btn_class: CONSTS.SUBMIT_BTN_CLASS, 309 widget_id: widgetId, 310 criteria: config.criteria, 311 max_stars: config.max_stars, 312 allow_multiple: config.allow_multiple || false, 313 criteria_scores: summary.criteria_scores || [], 314 avg_score: summary.avg_score || 0, 315 avg_score_pct: summary.avg_score_pct || 0, 316 total_evaluations: summary.total_evaluations || 0, 317 total_reviews: summary.total_reviews || 0, 318 user_evaluated: summary.user_scores && Object.keys(summary.user_scores).length > 0, 319 is_authenticated: isAuthenticated, 320 can_evaluate: userCanEvaluate, 321 can_evaluate_again: summary.can_evaluate_again !== false, 322 can_edit: summary.can_edit || false, 323 user_scores: summary.user_scores || {}, 324 user_sessions: (summary.user_sessions || []).map(function(s) { 325 var scores = s.scores || {}; 326 var vals = Object.values(scores); 327 var avg = vals.length > 0 328 ? vals.reduce(function(a, b) { return a + b; }, 0) / vals.length 329 : 0; 330 return Object.assign({}, s, { avg_score: Math.round(avg * 10) / 10 }); 331 }), 332 reviews: summary.reviews || [] 333 }; 334 335 $modalBody.html(nunjucks.render(CONSTS.TEMPLATE_DONE, nunjucksContext)); 336 337 // Quando o usuário não pode avaliar, troca a chamada de ação do header 338 // por uma frase de leitura (modal vira somente visualização das reviews). 339 if (!userCanEvaluate) { 340 $modal.find('.rating-evaluation-modal-sub').text(gettext("See what others are saying")); 341 } 342 343 // Inicializa rater readonly para a média geral no summary do lightbox 344 var $modalOverallStars = $modalBody.find('.plus_rating_modal_overall_stars'); 345 if ($modalOverallStars.length > 0) { 346 $modalOverallStars.attr("data-rate-value", summary.avg_score || 0).rate($.extend(true, {}, raterDefaultOptions, { 347 max_value: config.max_stars, 348 readonly: true, 349 step_size: 0.1 350 })); 351 } 352 353 // Inicializa rater para cada critério 354 if (userCanEvaluate) {
355 config.criteria.forEach(function(criterion) { 356 var $rater = $modalBody.find('.plus_rating_evaluation_criterion_' + criterion.id); 357 if ($rater.length === 0) return; 358 359 var initialValue = 0; 360 if (!config.allow_multiple && summary.user_scores) { 361 initialValue = summary.user_scores[criterion.id] || 0; 362 } 363 364 $rater.attr("data-rate-value", initialValue).rate($.extend(true, {}, raterDefaultOptions, { 365 max_value: config.max_stars 366 })); 367 }); 368 } 369 370 // Opções dos raters readonly para avg_score nas sessões e reviews 371 var avgStarOptions = $.extend(true, {}, raterDefaultOptions, { 372 max_value: config.max_stars, 373 readonly: true, 374 step_size: 0.1 375 }); 376 377 // Inicializa raters readonly de avg_score nas sessões do usuário 378 nunjucksContext.user_sessions.forEach(function(session, index) { 379 if (session.avg_score) { 380 var $rater = $modalBody.find('.plus_rating_session_avg_stars_' + index); 381 if ($rater.length > 0) { 382 $rater.attr('data-rate-value', session.avg_score).rate(avgStarOptions); 383 } 384 } 385 }); 386 387 // Inicializa raters readonly de avg_score nas reviews de outros usuários 388 (summary.reviews || []).forEach(function(review, index) { 389 if (review.avg_score) { 390 var $rater = $modalBody.find('.plus_rating_review_avg_stars_' + index); 391 if ($rater.length > 0) { 392 $rater.attr('data-rate-value', review.avg_score).rate(avgStarOptions); 393 } 394 } 395 }); 396 397 var $message = $modalBody.find('.' + CONSTS.MESSAGE_CLASS); 398 399 // --- Função para recarregar o modal após uma ação --- 400 function refreshModal() { 401 $modalBody.html(nunjucks.render(CONSTS.TEMPLATE_LOADING, {})); 402 $.ajax({ 403 // nocache=1 só nesse fluxo de refresh pós-mutação â bypass o cache do 404 // browser (HTTP max-age=5 servido pelo middleware) que de outra forma 405 // devolveria user_sessions/avg_score em estado pré-mutação. 406 url: Urls[CONSTS.API_SUMMARY_URL](), 407 data: { content_type: contentType, object_id: objectId, nocache: 1 }, 408 method: "GET", 409 dataType: "json" 410 }).done(function(summaryData) { 411 renderModalContent($modal, $container, config, summaryData); 412 // Atualiza o badge inline também 413 refreshBadge($container); 414 }).fail(function() { 415 $modalBody.html(nunjucks.render(CONSTS.TEMPLATE_FAIL, {})); 416 }); 417 } 418 419 // --- Handler de submit --- 420 var $submitBtn = $modalBody.find('.' + CONSTS.SUBMIT_BTN_CLASS); 421 $submitBtn.on('click', function(e) { 422 e.preventDefault(); 423 $submitBtn.prop('disabled', true); 424 $message.addClass('d-none'); 425 426 var result = collectScores($modalBody, config, 'plus_rating_evaluation_criterion_'); 427 if (!result.allRated) { 428 $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_ERROR, { 429 custom_message: gettext("Please rate all criteria before submitting.") 430 })); 431 $submitBtn.prop('disabled', false); 432 return; 433 } 434 435 var $reviewTextarea = $modalBody.find('.' + CONSTS.REVIEW_BOX_CLASS + ' textarea'); 436 var reviewText = $reviewTextarea.length > 0 ? $reviewTextarea.val() : ''; 437 var $isPublicCheck = $modalBody.find('.plus_rating_evaluation_is_public'); 438 var isPublic = $isPublicCheck.length > 0 ? $isPublicCheck.is(':checked') : true; 439 440 $.ajax({ 441 url: Urls[CONSTS.API_SUBMIT_URL](), 442 method: "POST", 443 contentType: "application/json", 444 data: JSON.stringify({ 445 content_type: contentType, 446 object_id: objectId, 447 scores: result.scores, 448 review_text: reviewText, 449 is_public: isPublic 450 }), 451 headers: { "X-CSRFToken": Looper.getCsrftoken() }, 452 dataType: "json" 453 }).done(function() { 454 $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_SUCCESS, {})); 455 setTimeout(refreshModal, 1500); 456 }).fail(function(xhr) { 457 $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_ERROR, { 458 custom_message: getErrorMessage(xhr) 459 })); 460 $submitBtn.prop('disabled', false); 461 }); 462 }); 463 464 // --- Handler de delete sessão --- 465 $modalBody.find('.plus_rating_evaluation_delete_session').on('click', function(e) { 466 e.preventDefault(); 467 var $btn = $(this); 468 var sessionId = $btn.data('session-id'); 469 470 showConfirmModal( 471 gettext("Delete evaluation"), 472 gettext("Are you sure you want to delete this evaluation?"), 473 function() { 474 $btn.prop('disabled', true); 475 $.ajax({ 476 url: Urls[CONSTS.API_DELETE_SESSION_URL](), 477 method: "POST", 478 contentType: "application/json", 479 data: JSON.stringify({ session_id: sessionId }), 480 headers: { "X-CSRFToken": Looper.getCsrftoken() }, 481 dataType: "json" 482 }).done(function() { 483 refreshModal(); 484 refreshBadge($container); 485 }).fail(function() { 486 $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_ERROR, { 487 custom_message: gettext("Failed to delete evaluation.") 488 })); 489 $btn.prop('disabled', false); 490 }); 491 } 492 ); 493 }); 494 495 // --- Handler de toggle public/private --- 496 $modalBody.find('.plus_rating_evaluation_toggle_public').on('click', function(e) { 497 e.preventDefault(); 498 var $btn = $(this); 499 var sessionId = $btn.data('session-id'); 500 var currentIsPublic = !!parseInt($btn.data('is-public'), 10); 501 var newIsPublic = !currentIsPublic; 502 503 $btn.prop('disabled', true); 504 505 $.ajax({ 506 url: Urls[CONSTS.API_TOGGLE_REVIEW_PUBLIC_URL](), 507 method: "POST", 508 contentType: "application/json", 509 data: JSON.stringify({ 510 session_id: sessionId, 511 is_public: newIsPublic 512 }), 513 headers: { "X-CSRFToken": Looper.getCsrftoken() }, 514 dataType: "json" 515 }).done(function() { 516 refreshModal(); 517 }).fail(function(xhr) { 518 console.error("Toggle review visibility failed:", xhr.status, xhr.responseText); 519 $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_ERROR, { 520 custom_message: getErrorMessage(xhr) || gettext("Failed to update review visibility.") 521 })); 522 $btn.prop('disabled', false); 523 }); 524 }); 525 526 // --- Handler de edição de review (texto + estrelas) --- 527 $modalBody.find('.plus_rating_evaluation_edit_review').on('click', function(e) { 528 e.preventDefault(); 529 var $btn = $(this); 530 var sessionId = $btn.data('session-id'); 531 var currentText = $btn.data('review-text'); 532 var currentIsPublic = !!parseInt($btn.data('is-public'), 10); 533 var $sessionDiv = $btn.closest('.plus_rating_user_session_card'); 534 var $textEl = $sessionDiv.find('.plus_rating_evaluation_review_text'); 535 536 var sessionScores = getSessionScores(summary, sessionId); 537 538 // Monta bloco de edição: estrelas + texto + visibilidade 539 var $editBlock = $('<div class="plus_rating_evaluation_edit_block"></div>'); 540 541 // Critérios com raters para edição de scores 542 if (summary.can_edit) { 543 var $criteriaBox = $('<div class="mb-2"></div>');
544 config.criteria.forEach(function(criterion) { 545 var initialValue = sessionScores[criterion.id] || 0; 546 var $criterionDiv = $('<div class="plus_rating_evaluation_criterion mb-1">' + 547 '<label class="d-block mb-0 small font-weight-bold">' + criterion.name + '</label>' + 548 '<div class="plus_rating_evaluation_inline_criterion_' + criterion.id + '"></div></div>'); 549 $criteriaBox.append($criterionDiv); 550 }); 551 $editBlock.append($criteriaBox); 552 } 553 554 var $textarea = $('<textarea class="form-control form-control-sm mb-1" rows="2"></textarea>').val(currentText); 555 var $isPublicCheck = $('<div class="form-check mt-1"><input type="checkbox" class="form-check-input" ' + 556 (currentIsPublic ? 'checked' : '') + '><label class="form-check-label small">' + 557 gettext("Make my review public") + '</label></div>'); 558 var $saveBtn = $('<button type="button" class="btn btn-sm btn-primary mt-1 mr-1">' + 559 gettext("Save") + '</button>'); 560 var $cancelBtn = $('<button type="button" class="btn btn-sm btn-secondary mt-1">' + 561 gettext("Cancel") + '</button>'); 562 563 $editBlock.append($textarea).append($isPublicCheck) 564 .append($('<div class="mt-1"></div>').append($saveBtn).append($cancelBtn)); 565 566 if ($textEl.length > 0) { 567 $textEl.replaceWith($editBlock); 568 } else { 569 $sessionDiv.find('.flex-grow-1').html($editBlock); 570 } 571 $btn.hide(); 572 573 // Inicializa raters após inserir no DOM 574 if (summary.can_edit) { 575 initSessionRaters($editBlock, config, sessionScores, 'plus_rating_evaluation_inline_criterion_'); 576 } 577 578 $cancelBtn.on('click', function() { 579 refreshModal(); 580 }); 581 582 $saveBtn.on('click', function() { 583 var newText = $textarea.val().trim(); 584 if (!newText) return; 585 $saveBtn.prop('disabled', true); 586 587 // Coleta scores se os raters existem 588 var hasScores = summary.can_edit && config.criteria.length > 0; 589 var scores = {}; 590 591 if (hasScores) { 592 var result = collectScores($editBlock, config, 'plus_rating_evaluation_inline_criterion_'); 593 scores = result.scores; 594 595 if (!result.allRated) { 596 $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_ERROR, { 597 custom_message: gettext("Please rate all criteria before submitting.") 598 })); 599 $saveBtn.prop('disabled', false); 600 return; 601 } 602 } 603 604 // Salva review + scores em paralelo 605 var reviewReq = $.ajax({ 606 url: Urls[CONSTS.API_EDIT_REVIEW_URL](), 607 method: "POST", 608 contentType: "application/json", 609 data: JSON.stringify({ 610 session_id: sessionId, 611 review_text: newText, 612 is_public: $isPublicCheck.find('input').is(':checked') 613 }), 614 headers: { "X-CSRFToken": Looper.getCsrftoken() }, 615 dataType: "json" 616 }); 617 618 var requests = [reviewReq]; 619 620 if (hasScores) { 621 var evalReq = $.ajax({ 622 url: Urls[CONSTS.API_EDIT_EVALUATION_URL](), 623 method: "POST", 624 contentType: "application/json", 625 data: JSON.stringify({ 626 content_type: contentType, 627 object_id: objectId, 628 session_id: sessionId, 629 scores: scores 630 }), 631 headers: { "X-CSRFToken": Looper.getCsrftoken() }, 632 dataType: "json" 633 }); 634 requests.push(evalReq); 635 } 636 637 $.when.apply($, requests).done(function() { 638 refreshModal(); 639 refreshBadge($container); 640 }).fail(function(xhr) { 641 $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_ERROR, { 642 custom_message: getErrorMessage(xhr) 643 })); 644 $saveBtn.prop('disabled', false); 645 }); 646 }); 647 }); 648 649 // --- Handler de edição de scores (botão standalone por sessão) --- 650 $modalBody.find('.plus_rating_evaluation_edit_scores').on('click', function(e) { 651 e.preventDefault(); 652 var $btn = $(this); 653 var sessionId = $btn.data('session-id'); 654 $btn.hide(); 655 656 var currentScores = getSessionScores(summary, sessionId); 657 658 // Mostra os critérios com rater para edição 659 var $criteriaBox = $('<div class="' + CONSTS.CRITERIA_BOX_CLASS + ' mt-2"></div>');
660 config.criteria.forEach(function(criterion) { 661 var $criterionDiv = $('<div class="plus_rating_evaluation_criterion mb-2">' + 662 '<label class="d-block mb-1 small font-weight-bold">' + criterion.name + '</label>' + 663 '<div class="plus_rating_evaluation_edit_criterion_' + criterion.id + '"></div></div>'); 664 $criteriaBox.append($criterionDiv); 665 }); 666 667 var $saveBtn = $('<button type="button" class="btn btn-sm btn-primary mt-2 mr-1">' + 668 gettext("Save") + '</button>'); 669 var $cancelBtn = $('<button type="button" class="btn btn-sm btn-secondary mt-2">' + 670 gettext("Cancel") + '</button>'); 671 var $editContainer = $('<div class="plus_rating_evaluation_edit_scores_container"></div>') 672 .append($criteriaBox).append($saveBtn).append($cancelBtn); 673 674 $btn.after($editContainer); 675 676 // Inicializa raters com valores atuais 677 initSessionRaters($editContainer, config, currentScores, 'plus_rating_evaluation_edit_criterion_'); 678 679 $cancelBtn.on('click', function() { 680 refreshModal(); 681 }); 682 683 $saveBtn.on('click', function() { 684 $saveBtn.prop('disabled', true); 685 686 var result = collectScores($editContainer, config, 'plus_rating_evaluation_edit_criterion_'); 687 if (!result.allRated) { 688 $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_ERROR, { 689 custom_message: gettext("Please rate all criteria before submitting.") 690 })); 691 $saveBtn.prop('disabled', false); 692 return; 693 } 694 695 $.ajax({ 696 url: Urls[CONSTS.API_EDIT_EVALUATION_URL](), 697 method: "POST", 698 contentType: "application/json", 699 data: JSON.stringify({ 700 content_type: contentType, 701 object_id: objectId, 702 session_id: sessionId, 703 scores: result.scores 704 }), 705 headers: { "X-CSRFToken": Looper.getCsrftoken() }, 706 dataType: "json" 707 }).done(function() { 708 $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_SUCCESS, {})); 709 setTimeout(refreshModal, 1500); 710 refreshBadge($container); 711 }).fail(function(xhr) { 712 $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_ERROR, { 713 custom_message: getErrorMessage(xhr) 714 })); 715 $saveBtn.prop('disabled', false); 716 }); 717 }); 718 }); 719 } 720 721 // ========================================================================= 722 // FASE 1 â Inicialização batch (summary-light) 723 // ========================================================================= 724 725 /** 726 * Atualiza o badge inline de um container (após ação no modal). 727 */ 728 function refreshBadge($container) { 729 var contentType = $container.attr(CONSTS.MODEL_ATTR); 730 var objectId = $container.attr(CONSTS.NODE_ID_ATTR); 731 732 $.ajax({ 733 // nocache=1 â chamado apenas no refresh pós-mutação para escapar do 734 // cache do browser (HTTP max-age=5) e refletir o avg_score atualizado. 735 url: Urls[CONSTS.API_SUMMARY_LIGHT_URL](), 736 data: { content_type: contentType, object_id: objectId, nocache: 1 }, 737 method: "GET", 738 dataType: "json" 739 }).done(function(data) { 740 renderBadge($container, data); 741 }); 742 } 743 744 /** 745 * Função principal que encontra e inicializa os badges inline. 746 */ 747 function initializeEvaluationComponents() { 748 if (isProcessing) return; 749 isProcessing = true; 750 751 var $unprocessed = $(CONSTS.CONTAINER_SELECTOR + ':not([' + CONSTS.PROCESSED_ATTR + '])'); 752 if ($unprocessed.length === 0) { 753 isProcessing = false; 754 return; 755 } 756 757 // Marca como processado o mais rápido possÃvel 758 $unprocessed.attr(CONSTS.PROCESSED_ATTR, 'true'); 759 760 var evaluationsToFetch = []; 761 var evaluationsDataMap = {}; 762 var counter = 0; 763 764 $unprocessed.each(function() { 765 var $el = $(this); 766 var evalData = { 767 content_type: $el.attr(CONSTS.MODEL_ATTR), 768 object_id: $el.attr(CONSTS.NODE_ID_ATTR) 769 }; 770 771 if (evalData.content_type && evalData.object_id) { 772 var fetchData = $.extend({}, evalData, { id: counter }); 773 evaluationsToFetch.push(fetchData); 774 evaluationsDataMap[counter] = { ref: $el, data: evalData }; 775 counter++; 776 } 777 }); 778 779 if (evaluationsToFetch.length === 0) { 780 isProcessing = false;
781 return; 782 } 783 784 // Usa summary-light para o batch (leve, sem reviews/scores) 785 var summaryUrl = Urls[CONSTS.API_SUMMARY_LIGHT_URL](); 786 var batches = Looper.chunkArrayByUrlSize(summaryUrl, CONSTS.API_KEY, evaluationsToFetch); 787 var ajaxPromises = []; 788 789 batches.forEach(function(batch) { 790 var request = $.ajax({ 791 url: summaryUrl, 792 data: { evaluation: JSON.stringify(batch) }, 793 method: "GET", 794 dataType: "json" 795 }).done(function(data) { 796 $.each(data, function(_, obj) { 797 if (obj.id === undefined || !evaluationsDataMap[obj.id]) return; 798 var mapped = evaluationsDataMap[obj.id]; 799 renderBadge(mapped.ref, obj); 800 }); 801 }).fail(function() { 802 batch.forEach(function(item) { 803 var mapped = evaluationsDataMap[item.id]; 804 if (mapped) { 805 mapped.ref.addClass(Looper.template_nunjucks_initialized_class); 806 mapped.ref.html(nunjucks.render(CONSTS.TEMPLATE_FAIL, {})); 807 } 808 }); 809 }); 810 811 ajaxPromises.push(request); 812 }); 813 814 $.when.apply($, ajaxPromises).always(function() { 815 isProcessing = false; 816 }); 817 } 818 819 // --- Inicialização --- 820 initializeEvaluationComponents(); 821 $(document).on("theme:newDomNodes", initializeEvaluationComponents); 822 823})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.