From 72768112c3dda085f450526715d42ea3bbeb2075 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Fri, 28 Nov 2025 10:09:39 -0300 Subject: [PATCH 01/70] feat: adjust in_use validation --- app/models/question.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/models/question.rb b/app/models/question.rb index 21b51c4fc..1fa78fda0 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -272,11 +272,7 @@ def owners?(s = false) end def in_use?(exam_id = nil) - if exam_id.nil? - exams.any? - else - (exams.ids.length > 1) - end + exams.exists? end def validate_images From 4285b1b28fbf573fd00b685d233e1fa50b1e1291 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Fri, 28 Nov 2025 10:10:48 -0300 Subject: [PATCH 02/70] feat: create used_by_multiple_exams validation in question.rb --- app/models/question.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/models/question.rb b/app/models/question.rb index 1fa78fda0..4b31ed72e 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -275,6 +275,11 @@ def in_use?(exam_id = nil) exams.exists? end + def used_by_multiple_exams? + exams.count > 1 + end + + def validate_images total = question_images.size + question_audios.size errors.add(:question, I18n.t('questions.error.max_files')) if question_images.any? && total > 4 From 9874e05fa2a220ab7596d8b06de753b1977703a0 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Fri, 28 Nov 2025 14:06:59 -0300 Subject: [PATCH 03/70] feat: create can_questions_be_edited? validation in exams.rb --- app/models/exam.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/models/exam.rb b/app/models/exam.rb index 9a0b6cf38..6c2a4bb94 100644 --- a/app/models/exam.rb +++ b/app/models/exam.rb @@ -495,6 +495,17 @@ def get_duration(total_time) #duration = (difference_minutes.to_i > self.duration.to_i) ? self.duration : total_time end + def can_questions_be_edited?(current_user) + has_acu = academic_allocation_users.any? + allocation_tag_ids = academic_allocations.pluck(:allocation_tag_id) + is_editor = current_user.profiles_with_access_on('update', 'questions', allocation_tag_ids, true).any? + + return false unless self.status + return false unless started? + return true unless has_acu + return is_editor + end + def current_time_db result = ActiveRecord::Base.connection.execute("SELECT current_time AS current_time") return Time.parse(result.to_a[0]['current_time']) From 87e156d66c84f4c9ec2958dac2742d6e35ca3d7b Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Fri, 28 Nov 2025 15:02:24 -0300 Subject: [PATCH 04/70] feat: adjust can_questions_be_edited? validation in exams.rb --- app/models/exam.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/models/exam.rb b/app/models/exam.rb index 6c2a4bb94..b608d616a 100644 --- a/app/models/exam.rb +++ b/app/models/exam.rb @@ -500,10 +500,9 @@ def can_questions_be_edited?(current_user) allocation_tag_ids = academic_allocations.pluck(:allocation_tag_id) is_editor = current_user.profiles_with_access_on('update', 'questions', allocation_tag_ids, true).any? - return false unless self.status - return false unless started? - return true unless has_acu - return is_editor + return true unless status + return true unless started? + return (is_editor && !has_acu) end def current_time_db From 98f5fc2858719fc634b3678a2b3db0ea23b8649e Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Fri, 28 Nov 2025 16:06:34 -0300 Subject: [PATCH 05/70] feat: adjust can_change? on question.rb with repository workflow --- app/models/exam.rb | 6 +++--- app/models/question.rb | 19 +++++++------------ 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/app/models/exam.rb b/app/models/exam.rb index b608d616a..cf9d31967 100644 --- a/app/models/exam.rb +++ b/app/models/exam.rb @@ -495,10 +495,10 @@ def get_duration(total_time) #duration = (difference_minutes.to_i > self.duration.to_i) ? self.duration : total_time end - def can_questions_be_edited?(current_user) + def can_questions_be_edited?(current_user, is_exam_question = false) has_acu = academic_allocation_users.any? - allocation_tag_ids = academic_allocations.pluck(:allocation_tag_id) - is_editor = current_user.profiles_with_access_on('update', 'questions', allocation_tag_ids, true).any? + allocation_tags = academic_allocations.pluck(:allocation_tag_id) + is_editor = is_exam_question ? true : current_user.profiles_with_access_on('update', 'questions', allocation_tags, true).any? return true unless status return true unless started? diff --git a/app/models/question.rb b/app/models/question.rb index 4b31ed72e..e23df26cb 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -246,13 +246,13 @@ def have_access? end - def can_change?(exam_id = nil) - raise 'permission' unless owner? + def can_change?(exam_id = nil, current_user) - if in_use?(exam_id) && status # if in use and already published - raise 'in_use_question' if exam_id.nil? #if on repository page - raise 'in_use_exam_question' #if on exams page - end + has_blocking_exam = exams.any? { |exam| !exam.can_questions_be_edited?(current_user, !exam_id.nil?) } + + return nil if has_blocking_exam + return true if owner? + return false if privacy end def owner?(s = false) @@ -271,15 +271,10 @@ def owners?(s = false) end end - def in_use?(exam_id = nil) + def in_use? exams.exists? end - def used_by_multiple_exams? - exams.count > 1 - end - - def validate_images total = question_images.size + question_audios.size errors.add(:question, I18n.t('questions.error.max_files')) if question_images.any? && total > 4 From 57c794fcdd212f385e673b21db2ba6dab9b461b6 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 3 Dec 2025 13:12:21 -0300 Subject: [PATCH 06/70] feat: adjust can_change? on question.rb with repository workflow --- app/models/question.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/models/question.rb b/app/models/question.rb index e23df26cb..5a7e7a967 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -246,6 +246,9 @@ def have_access? end + # Retorna true se a questão pode ser editada + # Retorna false se a questão não pode ser editada + # Retorna nil caso uma cópia da questão deva ser feita para que ocorra a edição def can_change?(exam_id = nil, current_user) has_blocking_exam = exams.any? { |exam| !exam.can_questions_be_edited?(current_user, !exam_id.nil?) } @@ -253,6 +256,9 @@ def can_change?(exam_id = nil, current_user) return nil if has_blocking_exam return true if owner? return false if privacy + raise 'in_use' if !owner? && privacy + !has_blocking_exam && owner? + end def owner?(s = false) From b7defed5711341f8479e4664de6eb3f424595698 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 3 Dec 2025 13:13:28 -0300 Subject: [PATCH 07/70] feat: add edit verifications in questions_controller.rb --- app/controllers/questions_controller.rb | 33 +++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 2c9d64acc..7fda52e3d 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -74,6 +74,7 @@ def change_status end def edit + Rails.logger.info "\n\n\n Ooopppsss Entrei no edit\n\n\n" @question = Question.find(params[:id]) @question.question_images.build @question.question_labels.build @@ -86,6 +87,8 @@ def update authorize! :update, Question @question = Question.find params[:id] + "\n\n\n Id da questão 3: #{params[:id]} \n\n\n" + if !params['question_texts']['text'].blank? @question.question_text.text = params['question_texts']['text'] @question_text = @question.question_text @@ -136,8 +139,31 @@ def show def verify_owners question = Question.find params[:id] if params[:update] - question.can_change?(params[:exam_id]) - authorize! :update, Question + exam_id = params[:exam_id] + + begin + Rails.logger.info "\n\n\n Exam_id no controller: #{exam_id} \n\n\n" + can_change = question.can_change?(params[:exam_id], current_user) + + Rails.logger.info "\n\n\n Can change: #{can_change} \n\n\n" + + if can_change + authorize! :update, Question + render json: { action: 'edit' }, status: :ok + return + else + Rails.logger.info "\n\n\n Entrei no else do copy \n\n\n" + # alert_message = Question.owner? ? t('questions.error.in_use_question') : 'Somente o autor pode editar a questão. Deseja fazer uma cópia?' + # Rails.logger.info "\n\n\n Entrei no else do copy 1 \n\n\n" + render json: { action: 'copy', alert: 'Você não pode editar essa questão. Deseja fazer uma cópia?' }, status: :ok + Rails.logger.info "\n\n\n Entrei no else do copy 2 \n\n\n" + return + end + rescue => error + error_message = I18n.translate!("#{error.message}", scope: [:questions, :error], :raise => true) rescue t('general_message', scope: [:questions, :error]) + render json: { success: false, alert: error_message }, status: :forbidden + return + end end if params[:copy] question.can_copy? @@ -154,6 +180,9 @@ def verify_owners end def copy + Rails.logger.info "\n\n\n Entrei no copy\n\n\n" + "\n\n\n Id da questão 2: #{params[:id]} \n\n\n" + authorize! :copy, Question question = Question.find params[:id] From 0378705d4eb551fa887c751dc339d6c0d37e716f Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 3 Dec 2025 13:13:56 -0300 Subject: [PATCH 08/70] test: add test messages --- app/assets/javascripts/questions.js.erb | 70 ++++++++++++++----------- app/models/exam.rb | 8 +++ app/models/question.rb | 6 +-- 3 files changed, 50 insertions(+), 34 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index 843bb8f8a..1c846df87 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -204,43 +204,51 @@ function edit_question(icon, replace_list){ var base_verify_url = "<%=Rails.application.routes.url_helpers.verify_owners_question_path(':id')%>".replace(':id', questions_ids); $.get(exam_id ? (base_verify_url + `/${exam_id}`) : base_verify_url, function(data){ - var url_edit = $(icon).data('link-to-edit').replace(':id', ids); - $(icon).call_fancybox({ - href : url_edit, - open: true, - 'autoDimensions': false, - 'autoSize': false, - width: '700', - height: 'auto', - //maxHeight: '70%', - beforeClose: function() { clear_ckeditor(); } - }); - }).fail(function(data){ - var data = $.parseJSON(data.responseText); - if (!confirm(data.alert)) - return this; - - $.get("<%=Rails.application.routes.url_helpers.copy_verify_owners_question_path(':id')%>".replace(':id', questions_ids), function(data){ - var url_copy = $(icon).prev().data('link-to-copy').replace(':id', ids); - $(icon).prev().call_fancybox({ href : url_copy, open: true, maxHeight: '70%', 'autoDimensions': false, 'autoSize': false, width: '700', height: 'auto', }); - if(replace_list != ''){ - $.get($(replace_list).data("link-list"), function(data2){ - $(replace_list).replaceWith(data2); - flash_message(data.notice, 'notice'); - update_nice_checkbox(); - }); - } - }).fail(function(data){ - var data = $.parseJSON(data.responseText); - if (typeof(data.alert) != "undefined") - flash_message(data.alert, 'alert'); - }); + console.log(data.action); + + if (data.action === 'copy'){ + + if (!confirm(data.alert)) + return this; + + console.log("Entrei no copy"); + var url_copy = $(icon).prev().data('link-to-copy').replace(':id', ids); + $(icon).prev().call_fancybox({ href : url_copy, open: true, maxHeight: '70%', 'autoDimensions': false, 'autoSize': false, width: '700', height: 'auto', }); + if(replace_list != ''){ + $.get($(replace_list).data("link-list"), function(data2){ + $(replace_list).replaceWith(data2); + flash_message(data.notice, 'notice'); + update_nice_checkbox(); + }); + } + } else { + console.log("Entrei no else ou edit"); + var url_edit = $(icon).data('link-to-edit').replace(':id', ids); + $(icon).call_fancybox({ + href : url_edit, + open: true, + 'autoDimensions': false, + 'autoSize': false, + width: '700', + height: 'auto', + //maxHeight: '70%', + beforeClose: function() { clear_ckeditor(); } + }); + } + }).fail(function(data){ + var data = $.parseJSON(data.responseText); + if (typeof(data.alert) != "undefined") + flash_message(data.alert, 'alert'); }); + }else{ + flash_message("<%=I18n.t('questions.error.choose_one')%>", "alert"); + return false; } } function copy_question(icon, replace_list){ + if ($(icon).attr('disabled') == 'disabled'){ flash_message("<%=I18n.t('questions.error.choose_one')%>", "alert"); return false; diff --git a/app/models/exam.rb b/app/models/exam.rb index cf9d31967..145ef5026 100644 --- a/app/models/exam.rb +++ b/app/models/exam.rb @@ -495,13 +495,21 @@ def get_duration(total_time) #duration = (difference_minutes.to_i > self.duration.to_i) ? self.duration : total_time end + # Retorna true se prova em rascunho ou não iniciou ou iniciou sem ACU e user é editor da turma em que a prova é usada + # Retorna false se prova publicada e encerrada ou iniciou com ACU ou iniciou com ACU e user não é editor da turma def can_questions_be_edited?(current_user, is_exam_question = false) + Rails.logger.info "\n\n\n Id do exam: #{id} \n\n\n" + has_acu = academic_allocation_users.any? allocation_tags = academic_allocations.pluck(:allocation_tag_id) is_editor = is_exam_question ? true : current_user.profiles_with_access_on('update', 'questions', allocation_tags, true).any? + Rails.logger.info "\n\n\n Status: #{status} \n\n\n" return true unless status + Rails.logger.info "\n\n\n Started: #{started?} \n\n\n" return true unless started? + Rails.logger.info "\n\n\n Is_editor e has_acu: #{is_editor} e #{!has_acu} = #{is_editor && !has_acu} \n\n\n" + return (is_editor && !has_acu) end diff --git a/app/models/question.rb b/app/models/question.rb index 5a7e7a967..b3e37857e 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -113,6 +113,7 @@ def dup_file(file, type) end def self.copy(question_to_copy, user_id = nil) + Rails.logger.info "\n\n\n Entrei no Question.copy\n\n\n" attributes = (user_id != question_to_copy.user_id ? { updated_by_user_id: user_id } : {}) question = Question.create question_to_copy.attributes.except('id', 'updated_by_user_id').merge(attributes) question.copy_dependencies_from(question_to_copy, user_id) @@ -253,9 +254,8 @@ def can_change?(exam_id = nil, current_user) has_blocking_exam = exams.any? { |exam| !exam.can_questions_be_edited?(current_user, !exam_id.nil?) } - return nil if has_blocking_exam - return true if owner? - return false if privacy + Rails.logger.info "\n\n\n has_blocking_exam: #{has_blocking_exam} \n\n\n" + raise 'in_use' if !owner? && privacy !has_blocking_exam && owner? From 6d83ce6f8235dea7fe934d3a2f69ba7c9617793b Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Thu, 4 Dec 2025 13:21:47 -0300 Subject: [PATCH 09/70] doc: adjust code documentation --- app/models/question.rb | 4 ++-- config/locales/en_US.yml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/models/question.rb b/app/models/question.rb index b3e37857e..fed6ad090 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -248,8 +248,8 @@ def have_access? # Retorna true se a questão pode ser editada - # Retorna false se a questão não pode ser editada - # Retorna nil caso uma cópia da questão deva ser feita para que ocorra a edição + # Retorna false se deve ser copiada para ser editada + # Chama uma exceção caso a questão não possa ser editada def can_change?(exam_id = nil, current_user) has_blocking_exam = exams.any? { |exam| !exam.can_questions_be_edited?(current_user, !exam_id.nil?) } diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index fe19fffe3..b081e1e5e 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -4597,6 +4597,7 @@ en_US: in_use: "This question is being used on exams. We suggest you make a copy so you can modify it." in_use_question: "This question is being used on a test. Would you like to duplicate it for editing?" in_use_exam_question: "This question is being used on another exam. Would you like to duplicate it for editing?" + not_owner: "Only the author can edit the question. Do you want to make a copy?" privacy_in_use: "can't be defined as private, because is being used at exams. If you're the author and the question is in use only at your exams, we suggest that you remove it from the exam and change it at Repositories area. If the question is in use at others exams that are not yours, it's not possible to change it." privacy_permission: can't be defined as private. You don't have permission to execute this action. min_items: The question must have, at least, 3 items From 2ec4c148bf9ecf1b83b718c6e7aeda85d46867ea Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Thu, 4 Dec 2025 13:22:35 -0300 Subject: [PATCH 10/70] feat: adjust error message when copy question --- app/controllers/questions_controller.rb | 9 +++++---- config/locales/pt_BR.yml | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 7fda52e3d..8f5a9dc89 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -143,7 +143,7 @@ def verify_owners begin Rails.logger.info "\n\n\n Exam_id no controller: #{exam_id} \n\n\n" - can_change = question.can_change?(params[:exam_id], current_user) + can_change = question.can_change?(exam_id, current_user) Rails.logger.info "\n\n\n Can change: #{can_change} \n\n\n" @@ -153,9 +153,10 @@ def verify_owners return else Rails.logger.info "\n\n\n Entrei no else do copy \n\n\n" - # alert_message = Question.owner? ? t('questions.error.in_use_question') : 'Somente o autor pode editar a questão. Deseja fazer uma cópia?' - # Rails.logger.info "\n\n\n Entrei no else do copy 1 \n\n\n" - render json: { action: 'copy', alert: 'Você não pode editar essa questão. Deseja fazer uma cópia?' }, status: :ok + + alert_message = question.owner? ? t('questions.error.in_use_question') : t('questions.error.not_owner') + Rails.logger.info "\n\n\n Entrei no else do copy 1 \n\n\n" + render json: { action: 'copy', alert: alert_message }, status: :ok Rails.logger.info "\n\n\n Entrei no else do copy 2 \n\n\n" return end diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index 2308ddd11..4e0510b9d 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -4922,6 +4922,7 @@ pt_BR: in_use: "Essa questão está sendo utilizada em provas. Sugerimos que faça uma cópia para que possa modificá-la." in_use_question: "Essa questão está sendo utilizada em uma prova. Deseja duplicá-la para fazer a edição?" in_use_exam_question: "Essa questão está sendo utilizada em outra prova. Deseja duplicá-la para fazer a edição?" + not_owner: "Somente o autor pode editar a questão. Deseja fazer uma cópia?" privacy_in_use: "não pode ser definida como privada, pois está sendo usada em provas. Se você é o autor e a questão está em uso apenas em sua prova, sugerimos que remova-a de sua prova e se dirija ao repositório para alterá-la. Caso a questão esteja em uso em outras provas não criadas por você, não é possível alterá-la" privacy_permission: não pode ser definida como privada. Você não tem permissão para realizar esta ação. min_items: A questão precisa ter, ao menos, 3 itens From 8dbebe53e978fca2330b7996ff0470e8d681d157 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Thu, 4 Dec 2025 13:41:30 -0300 Subject: [PATCH 11/70] feat: adjust error message when copy question in a started exam --- config/locales/en_US.yml | 2 +- config/locales/pt_BR.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index b081e1e5e..6fc6b3230 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -4595,7 +4595,7 @@ en_US: labels: 'labels: ' error: in_use: "This question is being used on exams. We suggest you make a copy so you can modify it." - in_use_question: "This question is being used on a test. Would you like to duplicate it for editing?" + in_use_question: "This question is being used on a started test. Would you like to duplicate it for editing?" in_use_exam_question: "This question is being used on another exam. Would you like to duplicate it for editing?" not_owner: "Only the author can edit the question. Do you want to make a copy?" privacy_in_use: "can't be defined as private, because is being used at exams. If you're the author and the question is in use only at your exams, we suggest that you remove it from the exam and change it at Repositories area. If the question is in use at others exams that are not yours, it's not possible to change it." diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index 4e0510b9d..1ba4aa19b 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -4920,7 +4920,7 @@ pt_BR: labels: 'palavras-chave: ' error: in_use: "Essa questão está sendo utilizada em provas. Sugerimos que faça uma cópia para que possa modificá-la." - in_use_question: "Essa questão está sendo utilizada em uma prova. Deseja duplicá-la para fazer a edição?" + in_use_question: "Essa questão está sendo utilizada em uma prova já iniciada. Deseja duplicá-la para fazer a edição?" in_use_exam_question: "Essa questão está sendo utilizada em outra prova. Deseja duplicá-la para fazer a edição?" not_owner: "Somente o autor pode editar a questão. Deseja fazer uma cópia?" privacy_in_use: "não pode ser definida como privada, pois está sendo usada em provas. Se você é o autor e a questão está em uso apenas em sua prova, sugerimos que remova-a de sua prova e se dirija ao repositório para alterá-la. Caso a questão esteja em uso em outras provas não criadas por você, não é possível alterá-la" From 2623d7e34fc1a5b6f27243ac11088d014553979b Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Fri, 5 Dec 2025 09:26:01 -0300 Subject: [PATCH 12/70] feat: adjust update permissions and remove console messages --- app/assets/javascripts/questions.js.erb | 5 ----- app/controllers/questions_controller.rb | 23 ++++++----------------- app/models/exam.rb | 6 ------ app/models/question.rb | 3 --- 4 files changed, 6 insertions(+), 31 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index 1c846df87..88c20b7d1 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -204,15 +204,11 @@ function edit_question(icon, replace_list){ var base_verify_url = "<%=Rails.application.routes.url_helpers.verify_owners_question_path(':id')%>".replace(':id', questions_ids); $.get(exam_id ? (base_verify_url + `/${exam_id}`) : base_verify_url, function(data){ - - console.log(data.action); - if (data.action === 'copy'){ if (!confirm(data.alert)) return this; - console.log("Entrei no copy"); var url_copy = $(icon).prev().data('link-to-copy').replace(':id', ids); $(icon).prev().call_fancybox({ href : url_copy, open: true, maxHeight: '70%', 'autoDimensions': false, 'autoSize': false, width: '700', height: 'auto', }); if(replace_list != ''){ @@ -223,7 +219,6 @@ function edit_question(icon, replace_list){ }); } } else { - console.log("Entrei no else ou edit"); var url_edit = $(icon).data('link-to-edit').replace(':id', ids); $(icon).call_fancybox({ href : url_edit, diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 8f5a9dc89..201a8ae9f 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -74,7 +74,6 @@ def change_status end def edit - Rails.logger.info "\n\n\n Ooopppsss Entrei no edit\n\n\n" @question = Question.find(params[:id]) @question.question_images.build @question.question_labels.build @@ -87,8 +86,6 @@ def update authorize! :update, Question @question = Question.find params[:id] - "\n\n\n Id da questão 3: #{params[:id]} \n\n\n" - if !params['question_texts']['text'].blank? @question.question_text.text = params['question_texts']['text'] @question_text = @question.question_text @@ -142,29 +139,23 @@ def verify_owners exam_id = params[:exam_id] begin - Rails.logger.info "\n\n\n Exam_id no controller: #{exam_id} \n\n\n" - can_change = question.can_change?(exam_id, current_user) - - Rails.logger.info "\n\n\n Can change: #{can_change} \n\n\n" + can_edit = question.can_change?(exam_id, current_user) - if can_change + if can_edit authorize! :update, Question render json: { action: 'edit' }, status: :ok return else - Rails.logger.info "\n\n\n Entrei no else do copy \n\n\n" - - alert_message = question.owner? ? t('questions.error.in_use_question') : t('questions.error.not_owner') - Rails.logger.info "\n\n\n Entrei no else do copy 1 \n\n\n" - render json: { action: 'copy', alert: alert_message }, status: :ok - Rails.logger.info "\n\n\n Entrei no else do copy 2 \n\n\n" + alert_key = question.owner? ? 'in_use_question' : 'not_owner' + render json: { action: 'copy', alert: I18n.t(alert_key, scope: [:questions, :error]) }, status: :ok return end - rescue => error + rescue RuntimeError => error error_message = I18n.translate!("#{error.message}", scope: [:questions, :error], :raise => true) rescue t('general_message', scope: [:questions, :error]) render json: { success: false, alert: error_message }, status: :forbidden return end + end if params[:copy] question.can_copy? @@ -181,8 +172,6 @@ def verify_owners end def copy - Rails.logger.info "\n\n\n Entrei no copy\n\n\n" - "\n\n\n Id da questão 2: #{params[:id]} \n\n\n" authorize! :copy, Question diff --git a/app/models/exam.rb b/app/models/exam.rb index 145ef5026..b7d92d9bb 100644 --- a/app/models/exam.rb +++ b/app/models/exam.rb @@ -498,18 +498,12 @@ def get_duration(total_time) # Retorna true se prova em rascunho ou não iniciou ou iniciou sem ACU e user é editor da turma em que a prova é usada # Retorna false se prova publicada e encerrada ou iniciou com ACU ou iniciou com ACU e user não é editor da turma def can_questions_be_edited?(current_user, is_exam_question = false) - Rails.logger.info "\n\n\n Id do exam: #{id} \n\n\n" - has_acu = academic_allocation_users.any? allocation_tags = academic_allocations.pluck(:allocation_tag_id) is_editor = is_exam_question ? true : current_user.profiles_with_access_on('update', 'questions', allocation_tags, true).any? - Rails.logger.info "\n\n\n Status: #{status} \n\n\n" return true unless status - Rails.logger.info "\n\n\n Started: #{started?} \n\n\n" return true unless started? - Rails.logger.info "\n\n\n Is_editor e has_acu: #{is_editor} e #{!has_acu} = #{is_editor && !has_acu} \n\n\n" - return (is_editor && !has_acu) end diff --git a/app/models/question.rb b/app/models/question.rb index fed6ad090..21b308eb8 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -113,7 +113,6 @@ def dup_file(file, type) end def self.copy(question_to_copy, user_id = nil) - Rails.logger.info "\n\n\n Entrei no Question.copy\n\n\n" attributes = (user_id != question_to_copy.user_id ? { updated_by_user_id: user_id } : {}) question = Question.create question_to_copy.attributes.except('id', 'updated_by_user_id').merge(attributes) question.copy_dependencies_from(question_to_copy, user_id) @@ -254,8 +253,6 @@ def can_change?(exam_id = nil, current_user) has_blocking_exam = exams.any? { |exam| !exam.can_questions_be_edited?(current_user, !exam_id.nil?) } - Rails.logger.info "\n\n\n has_blocking_exam: #{has_blocking_exam} \n\n\n" - raise 'in_use' if !owner? && privacy !has_blocking_exam && owner? From 6523025c021a633f0208ec64c6ab85bab1a49400 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Mon, 8 Dec 2025 16:53:25 -0300 Subject: [PATCH 13/70] feat: create send_email_to_responsible_users function --- app/models/allocation.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/models/allocation.rb b/app/models/allocation.rb index 18eceaf89..fccb22056 100644 --- a/app/models/allocation.rb +++ b/app/models/allocation.rb @@ -475,6 +475,19 @@ def calculate_grade_and_hours end end + def self.send_email_to_responsible_users(allocation_tag_ids ,subject, message_body) + responsibles_list = responsibles(allocation_tag_ids) + emails = responsibles_list.each do |responsible| + responsible.user.email + end + clean_emails = emails.compact.uniq + + if emails.any? + Job.send_mass_email(clean_emails, subject, message_body) + end + + end + private def verify_profile From 4da7e7dca18ceefe4668533660e183403b846653 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Mon, 8 Dec 2025 16:54:21 -0300 Subject: [PATCH 14/70] feat: create notify_responsibles function --- app/models/exam.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/models/exam.rb b/app/models/exam.rb index b7d92d9bb..8f13602cd 100644 --- a/app/models/exam.rb +++ b/app/models/exam.rb @@ -507,6 +507,13 @@ def can_questions_be_edited?(current_user, is_exam_question = false) return (is_editor && !has_acu) end + def notify_responsibles(message) + allocation_tags = academic_allocations.pluck(:allocation_tag_id) + subject = I18n.t('exams.notification.subject', name: name, allocation_info: allocation_tag_info) + message_body = I18n.t('exams.notification.message_body', message: message, name: name, allocation_info: allocation_tag_info) + Allocation.send_email_to_responsible_users(allocation_tags, subject, message_body) + end + def current_time_db result = ActiveRecord::Base.connection.execute("SELECT current_time AS current_time") return Time.parse(result.to_a[0]['current_time']) From 15ed635915aa09dce0fa39c98d8fee9ba18879bf Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Mon, 8 Dec 2025 16:55:07 -0300 Subject: [PATCH 15/70] feat: adjust verify_edit_permission function on question.rb --- app/models/question.rb | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/models/question.rb b/app/models/question.rb index 21b308eb8..d4e0c9309 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -249,13 +249,16 @@ def have_access? # Retorna true se a questão pode ser editada # Retorna false se deve ser copiada para ser editada # Chama uma exceção caso a questão não possa ser editada - def can_change?(exam_id = nil, current_user) - - has_blocking_exam = exams.any? { |exam| !exam.can_questions_be_edited?(current_user, !exam_id.nil?) } + def verify_edit_permission(exam_id = nil, current_user) + has_blocking_exam = exams.any? do |exam| + !exam.can_questions_be_edited?(current_user, !exam_id.nil?) + end raise 'in_use' if !owner? && privacy - !has_blocking_exam && owner? + return :copy_blocked_exam if has_blocking_exam + return :copy_ownership unless owner? + exams.any? ? :edit_and_notify : :edit_full end def owner?(s = false) From 055f5185de4c43283ae2f5ec675812668fd54d50 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Mon, 8 Dec 2025 16:56:16 -0300 Subject: [PATCH 16/70] feat: adjust verify_owners function --- app/controllers/questions_controller.rb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 201a8ae9f..bad8ce27d 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -139,21 +139,23 @@ def verify_owners exam_id = params[:exam_id] begin - can_edit = question.can_change?(exam_id, current_user) + edition_status = question.verify_edit_permission(exam_id, current_user) - if can_edit + if [:edit_full, :edit_and_notify].include?(edition_status) authorize! :update, Question - render json: { action: 'edit' }, status: :ok + render json: { action: 'edit', notify: edition_status == :edit_and_notify, }, status: :ok return else - alert_key = question.owner? ? 'in_use_question' : 'not_owner' + alert_key = edition_status == :copy_blocked_exam ? 'in_use_question' : 'not_owner' render json: { action: 'copy', alert: I18n.t(alert_key, scope: [:questions, :error]) }, status: :ok return end rescue RuntimeError => error - error_message = I18n.translate!("#{error.message}", scope: [:questions, :error], :raise => true) rescue t('general_message', scope: [:questions, :error]) - render json: { success: false, alert: error_message }, status: :forbidden - return + if error.message == 'in_use' + error_message = I18n.translate!("#{error.message}", scope: [:questions, :error], :raise => true) rescue t('general_message', scope: [:questions, :error]) + render json: { success: false, alert: error_message }, status: :forbidden + return + end end end From d51f8c3cb23b275a174926567914d31b7fd8c32f Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Mon, 8 Dec 2025 17:01:46 -0300 Subject: [PATCH 17/70] feat: add e-mail notification --- app/assets/javascripts/questions.js.erb | 7 +++++++ app/controllers/questions_controller.rb | 17 ++++++++++++++--- app/views/questions/form/_steps.html.haml | 1 + 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index 88c20b7d1..5958e607a 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -220,6 +220,13 @@ function edit_question(icon, replace_list){ } } else { var url_edit = $(icon).data('link-to-edit').replace(':id', ids); + + if (data.notify) + url_edit += "?notify_responsibles=true"; + + if (!confirm("<%=I18n.t('questions.questions.notify_editors_alert')%>")) + return this; + $(icon).call_fancybox({ href : url_edit, open: true, diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index bad8ce27d..92a2556d2 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -86,13 +86,24 @@ def update authorize! :update, Question @question = Question.find params[:id] + text_changed = false + if !params['question_texts']['text'].blank? - @question.question_text.text = params['question_texts']['text'] - @question_text = @question.question_text - @question_text.save + @question_text = @question.question_text || @question.build_question_text + @question_text.text = params['question_texts']['text'] + if @question_text.save + text_changed = @question_text.previous_changes.present? + end end if @question.update_attributes question_params + if params[:notify_responsibles] == 'true' && (text_changed || @question.previous_changes.present?) + message = t('questions.questions.notification', question_enunciation: @question.enunciation) + @question.exams.each do |exam| + exam.notify_responsibles(message) + end + end + render partial: 'question', locals: { question: @question } else @errors = [] diff --git a/app/views/questions/form/_steps.html.haml b/app/views/questions/form/_steps.html.haml index 3b88444db..9591d37e9 100644 --- a/app/views/questions/form/_steps.html.haml +++ b/app/views/questions/form/_steps.html.haml @@ -1,5 +1,6 @@ .new_question = simple_form_for(@question, html: { multipart: true, id: 'question_form' }) do |f| + = hidden_field_tag :notify_responsibles, params[:notify_responsibles] %span.form_requirement= t(:required_fields) %h1#lightBoxDialogTitle= (f.object.persisted? ? t('questions.form.steps.edit') : t('questions.form.steps.new')) .question_form.block_content From 942fcbfe9f72d090994c0074e919174e16da2072 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Mon, 8 Dec 2025 17:02:20 -0300 Subject: [PATCH 18/70] feat: add e-mail notification --- config/locales/en_US.yml | 5 +++++ config/locales/pt_BR.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index 6fc6b3230..8903390ec 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -4436,6 +4436,9 @@ en_US: link: "Attempt %{number} (Grade: %{grade})" warning: title: "Important: You can't create, import or remove questions while exam's published" + notification: + subject: "Change in the Exam %{name} (%{allocation_info}) " + message_body: "%{message}Verify if any changes are needed for Exam %{name} (%{allocation_info})." question: question: 'Question ' enunciation: 'Enunciation ' @@ -4500,6 +4503,8 @@ en_US: hide_alt: "Hide audio's text version" score: Scores associated_text: Associated text + notification: "There was a change in the question with the statement '%{question_enunciation}'. " + notify_editors_alert: "This question is in a draft exam, or a exam that has not been started or that has not been answered. When it is modified, the editors of the class when the exam will be applied will be notified. Do you wish to continue?" question: preview: Preview public: Public diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index 1ba4aa19b..1aa6a93d5 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -4760,6 +4760,9 @@ pt_BR: link: "Tentativa %{number} (Nota: %{grade})" warning: title: 'Importante: Não é possível criar, importar ou remover questões em provas publicadas' + notification: + subject: "Alteração na Prova %{name} (%{allocation_info}) " + message_body: "%{message}Verifique se alterações na Prova %{name} (%{allocation_info}) são necessárias." score: 'Pontuação ' question: question: 'Questão ' @@ -4825,6 +4828,8 @@ pt_BR: hide_alt: Ocultar versão textual do áudio score: Pontuação associated_text: Texto associado + notification: "Houve uma alteração na questão com o enunciado '%{question_enunciation}'. " + notify_editors_alert: "Esta questão está em um rascunho de prova, ou seja, em uma prova que ainda não foi iniciada ou respondida. Quando for modificada, os editores da disciplina em que a prova será aplicada serão notificados. Deseja continuar?" question: preview: Visualização public: Pública From 35c76d7faadc355b962b1ca39bd1c2873392a6ab Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Tue, 9 Dec 2025 09:59:48 -0300 Subject: [PATCH 19/70] feat: create very_edit_permissions route --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 4476e60c2..17f7d2723 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -693,7 +693,7 @@ member do put :change_status put :publish, action: :change_status, status: true - get 'verify_owners(/:exam_id)', as: :verify_owners, action: :verify_owners, update: true + get 'verify_edit_permissions(/:exam_id)', as: :verify_edit_permissions, action: :verify_edit_permissions get :copy_verify_owners, action: :verify_owners, copy: true get :show_verify_owners, action: :verify_owners, show: true get :copy From 6f8dfdc48ea86a1957768982c69f409ceeaac4ef Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Tue, 9 Dec 2025 10:00:31 -0300 Subject: [PATCH 20/70] feat: add exam.nil? verification in verify_edit_permission function --- app/models/question.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/question.rb b/app/models/question.rb index d4e0c9309..83a606fac 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -251,6 +251,7 @@ def have_access? # Chama uma exceção caso a questão não possa ser editada def verify_edit_permission(exam_id = nil, current_user) has_blocking_exam = exams.any? do |exam| + next false if exam.nil? !exam.can_questions_be_edited?(current_user, !exam_id.nil?) end From 8a17a8c4f38087bb04587ed61381ae4e78c0ae87 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Tue, 9 Dec 2025 10:02:31 -0300 Subject: [PATCH 21/70] feat: adjust questions.js.erb and questions_controller.rb to verify_edit_permissions route --- app/assets/javascripts/questions.js.erb | 6 ++- app/controllers/questions_controller.rb | 50 ++++++++++++------------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index 5958e607a..c52908afc 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -201,7 +201,7 @@ function edit_question(icon, replace_list){ } if (!(!ids.length || ids.length > 1)) { - var base_verify_url = "<%=Rails.application.routes.url_helpers.verify_owners_question_path(':id')%>".replace(':id', questions_ids); + var base_verify_url = "<%=Rails.application.routes.url_helpers.verify_edit_permissions_question_path(':id')%>".replace(':id', questions_ids); $.get(exam_id ? (base_verify_url + `/${exam_id}`) : base_verify_url, function(data){ if (data.action === 'copy'){ @@ -221,11 +221,13 @@ function edit_question(icon, replace_list){ } else { var url_edit = $(icon).data('link-to-edit').replace(':id', ids); - if (data.notify) + if (data.notify) { url_edit += "?notify_responsibles=true"; if (!confirm("<%=I18n.t('questions.questions.notify_editors_alert')%>")) return this; + } + $(icon).call_fancybox({ href : url_edit, diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 92a2556d2..f0b761849 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -2,7 +2,7 @@ class QuestionsController < ApplicationController include SysLog::Actions - before_action :set_current_user, only: [:destroy, :change_status, :verify_owners, :copy, :index] + before_action :set_current_user, only: [:destroy, :change_status, :verify_owners, :verify_edit_permissions, :copy, :index] layout false, except: :index @@ -119,6 +119,30 @@ def update render_json_error(error, 'questions.error') end + def verify_edit_permissions + question = Question.find params[:id] + exam_id = params[:exam_id] + + edition_status = question.verify_edit_permission(exam_id, current_user) + + if [:edit_full, :edit_and_notify].include?(edition_status) + authorize! :update, Question + render json: { action: 'edit', notify: edition_status == :edit_and_notify, }, status: :ok + else + alert_key = edition_status == :copy_blocked_exam ? 'in_use_question' : 'not_owner' + render json: { action: 'copy', alert: I18n.t(alert_key, scope: [:questions, :error]) }, status: :ok + + end + + rescue RuntimeError => error + if error.message == 'in_use' + error_message = I18n.translate!("#{error.message}", scope: [:questions, :error], :raise => true) rescue t('general_message', scope: [:questions, :error]) + render json: { success: false, alert: error_message }, status: :forbidden + end + rescue => error + render_json_error(error, 'questions.error') + end + def destroy authorize! :destroy, Question ActiveRecord::Base.transaction do @@ -146,30 +170,6 @@ def show def verify_owners question = Question.find params[:id] - if params[:update] - exam_id = params[:exam_id] - - begin - edition_status = question.verify_edit_permission(exam_id, current_user) - - if [:edit_full, :edit_and_notify].include?(edition_status) - authorize! :update, Question - render json: { action: 'edit', notify: edition_status == :edit_and_notify, }, status: :ok - return - else - alert_key = edition_status == :copy_blocked_exam ? 'in_use_question' : 'not_owner' - render json: { action: 'copy', alert: I18n.t(alert_key, scope: [:questions, :error]) }, status: :ok - return - end - rescue RuntimeError => error - if error.message == 'in_use' - error_message = I18n.translate!("#{error.message}", scope: [:questions, :error], :raise => true) rescue t('general_message', scope: [:questions, :error]) - render json: { success: false, alert: error_message }, status: :forbidden - return - end - end - - end if params[:copy] question.can_copy? authorize! :copy, Question From 9d055e79d14db83901547a7aa9c43824186ee8de Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Tue, 9 Dec 2025 14:05:06 -0300 Subject: [PATCH 22/70] doc: adjust function documentation --- app/models/exam.rb | 6 +++--- app/models/question.rb | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/models/exam.rb b/app/models/exam.rb index 8f13602cd..5d13ebea9 100644 --- a/app/models/exam.rb +++ b/app/models/exam.rb @@ -495,8 +495,8 @@ def get_duration(total_time) #duration = (difference_minutes.to_i > self.duration.to_i) ? self.duration : total_time end - # Retorna true se prova em rascunho ou não iniciou ou iniciou sem ACU e user é editor da turma em que a prova é usada - # Retorna false se prova publicada e encerrada ou iniciou com ACU ou iniciou com ACU e user não é editor da turma + # Return true if the exam is in draft mode, has not started, or started without ACU, and the user is the editor of the class where the exam is being used. + # Return false if exam is published and closed, or started with ACU, or started without ACU and user is not a class editor. def can_questions_be_edited?(current_user, is_exam_question = false) has_acu = academic_allocation_users.any? allocation_tags = academic_allocations.pluck(:allocation_tag_id) @@ -504,7 +504,7 @@ def can_questions_be_edited?(current_user, is_exam_question = false) return true unless status return true unless started? - return (is_editor && !has_acu) + is_editor && !has_acu end def notify_responsibles(message) diff --git a/app/models/question.rb b/app/models/question.rb index 83a606fac..17df8027f 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -246,9 +246,9 @@ def have_access? end - # Retorna true se a questão pode ser editada - # Retorna false se deve ser copiada para ser editada - # Chama uma exceção caso a questão não possa ser editada + # Return true if question can be directly edited + # Return fase if question must be copied to be edited + # Raise an exception if the question cannot be edited def verify_edit_permission(exam_id = nil, current_user) has_blocking_exam = exams.any? do |exam| next false if exam.nil? From 9860f7ac2c2e829a3f014c7236209fa8f61bad95 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 10 Dec 2025 08:12:58 -0300 Subject: [PATCH 23/70] fix: remove correction info from repository page --- app/views/questions/form/_info.html.haml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/views/questions/form/_info.html.haml b/app/views/questions/form/_info.html.haml index 6316dc9dc..e5b8b296b 100644 --- a/app/views/questions/form/_info.html.haml +++ b/app/views/questions/form/_info.html.haml @@ -14,18 +14,18 @@ - unless eq.nil? = eq.input :score, input_html: { step: '0.5' } - .correction_info - .expand_correction - =link_to('#void', onclick: 'toggle_text(this);' , onkeydown: 'click_on_keypress(event, this); ') do - = t('questions.question.understanding_score_calculation') - %i.icon-arrow-down-triangle{:'aria-hidden'=>'true'} - .correction_text - .invisible.true_or_false_calculation - = raw(t('questions.types.true_or_false_calculation', score_item: t('questions.question.total_amount'))) - .invisible.unique_choice_calculation - = raw(t('questions.types.unique_choice_calculation', score_item: t('questions.question.total_amount_per_items'))) - .invisible.multiple_choice_new_calculation - = raw(t('questions.types.multiple_choice_new_calculation', score_item: t('questions.question.total_amount_per_items'))) + .correction_info + .expand_correction + =link_to('#void', onclick: 'toggle_text(this);' , onkeydown: 'click_on_keypress(event, this); ') do + = t('questions.question.understanding_score_calculation') + %i.icon-arrow-down-triangle{:'aria-hidden'=>'true'} + .correction_text + .invisible.true_or_false_calculation + = raw(t('questions.types.true_or_false_calculation', score_item: t('questions.question.total_amount'))) + .invisible.unique_choice_calculation + = raw(t('questions.types.unique_choice_calculation', score_item: t('questions.question.total_amount_per_items'))) + .invisible.multiple_choice_new_calculation + = raw(t('questions.types.multiple_choice_new_calculation', score_item: t('questions.question.total_amount_per_items'))) .labels = f.label t('questions.questions.labels'), class: 'label_labels' From 17376d6c8c05bd7b91ea5745f21b4c58ac7ee9ab Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 10 Dec 2025 09:52:44 -0300 Subject: [PATCH 24/70] feat: adjust label input and delete button behavior on add/edit question form --- .../stylesheets/partials/_questions.scss | 35 +++++++++++++------ app/views/questions/form/_info.html.haml | 7 ++-- app/views/questions/form/_label.html.haml | 4 +-- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/app/assets/stylesheets/partials/_questions.scss b/app/assets/stylesheets/partials/_questions.scss index 42848a9a9..ac56af9a2 100644 --- a/app/assets/stylesheets/partials/_questions.scss +++ b/app/assets/stylesheets/partials/_questions.scss @@ -590,32 +590,47 @@ } .labels { + display: flex; + flex-direction: row; + + #labels_container { + flex-direction: column; + } + + label { + flex-shrink: 0; + } + input { width: 100px; } .nested_question_question_labels, .nested_exam_question_question_question_labels { - display: inline-block; margin-right: 10px; - a.remove_nested_fields_link { - float: right; + .duplicatable_nested_form { + display: flex; + flex-direction: row; + justify-content: end; + margin-top: 10px; + + a.remove_nested_fields_link { + margin-left: 2px; + margin-top: 2px; + } } } #labels { - .nested_exam_question_question_question_labels { - .input.string { - display: inline; - } - } + display: flex; + flex-direction: row; + flex-wrap: wrap; } } .add_nested_fields_link { - margin: 10px; - margin-left: 35px + margin: 10px 0; } } diff --git a/app/views/questions/form/_info.html.haml b/app/views/questions/form/_info.html.haml index e5b8b296b..e8bd31a01 100644 --- a/app/views/questions/form/_info.html.haml +++ b/app/views/questions/form/_info.html.haml @@ -29,9 +29,10 @@ .labels = f.label t('questions.questions.labels'), class: 'label_labels' - = render partial: 'questions/form/label', locals: { f: f } - .input.string - = f.add_nested_fields_link :question_labels, t('.add_labels'), class: 'btn' + %div#labels_container + = render partial: 'questions/form/label', locals: { f: f } + .input.string + = f.add_nested_fields_link :question_labels, t('.add_labels'), class: 'btn' = f.input :enunciation, as: :ckeditor diff --git a/app/views/questions/form/_label.html.haml b/app/views/questions/form/_label.html.haml index c6dedca0c..96d1514d4 100644 --- a/app/views/questions/form/_label.html.haml +++ b/app/views/questions/form/_label.html.haml @@ -1,11 +1,11 @@ -%span#labels +%div#labels - count = 0 - count_labels = f.object.question_labels.count = f.nested_fields_for :question_labels, wrapper_tag: :div do |label| - if (f.object.new_record? || label.object.persisted? || count == count_labels + 1) - count += 1 .duplicatable_nested_form - = label.remove_nested_fields_link content_tag(:i, nil, class: 'icon-trash') = label.input :name, label: false + = label.remove_nested_fields_link content_tag(:i, nil, class: 'icon-trash') -else - count += 1 \ No newline at end of file From 86cc7b3def3d62c0305f8f7dde35f9edd6cb8a0e Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 10 Dec 2025 11:15:39 -0300 Subject: [PATCH 25/70] fix: adjust correction_info behavior --- app/views/questions/form/_info.html.haml | 30 +++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/app/views/questions/form/_info.html.haml b/app/views/questions/form/_info.html.haml index e8bd31a01..c2fe1f053 100644 --- a/app/views/questions/form/_info.html.haml +++ b/app/views/questions/form/_info.html.haml @@ -14,18 +14,18 @@ - unless eq.nil? = eq.input :score, input_html: { step: '0.5' } - .correction_info - .expand_correction - =link_to('#void', onclick: 'toggle_text(this);' , onkeydown: 'click_on_keypress(event, this); ') do - = t('questions.question.understanding_score_calculation') - %i.icon-arrow-down-triangle{:'aria-hidden'=>'true'} - .correction_text - .invisible.true_or_false_calculation - = raw(t('questions.types.true_or_false_calculation', score_item: t('questions.question.total_amount'))) - .invisible.unique_choice_calculation - = raw(t('questions.types.unique_choice_calculation', score_item: t('questions.question.total_amount_per_items'))) - .invisible.multiple_choice_new_calculation - = raw(t('questions.types.multiple_choice_new_calculation', score_item: t('questions.question.total_amount_per_items'))) + .correction_info + .expand_correction + =link_to('#void', onclick: 'toggle_text(this);' , onkeydown: 'click_on_keypress(event, this); ') do + = t('questions.question.understanding_score_calculation') + %i.icon-arrow-down-triangle{:'aria-hidden'=>'true'} + .correction_text + .invisible.true_or_false_calculation + = raw(t('questions.types.true_or_false_calculation', score_item: t('questions.question.total_amount'))) + .invisible.unique_choice_calculation + = raw(t('questions.types.unique_choice_calculation', score_item: t('questions.question.total_amount_per_items'))) + .invisible.multiple_choice_new_calculation + = raw(t('questions.types.multiple_choice_new_calculation', score_item: t('questions.question.total_amount_per_items'))) .labels = f.label t('questions.questions.labels'), class: 'label_labels' @@ -113,7 +113,7 @@ $("#question_texts_media_question").attr("checked","true"); } - $('#exam_question_question_attributes_type_question')[0].addEventListener("change", (event) => { + $('#exam_question_question_attributes_type_question, #question_type_question')[0].addEventListener("change", (event) => { if ($('.correction_text').children(':visible').length>0){ $('.expand_correction a').click(); } @@ -207,7 +207,9 @@ } function toggle_text(link){ - type = $('#exam_question_question_attributes_type_question').val(); + type = $('#exam_question_question_attributes_type_question').length + ? $('#exam_question_question_attributes_type_question').val() + : $('#question_type_question').val(); div = $('.correction_text').children(':visible'); From d7404ebd91796c4267cb7c64c0444780d3bd14f0 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 10 Dec 2025 12:27:32 -0300 Subject: [PATCH 26/70] refactor: adjust item data-tooltip --- app/assets/stylesheets/partials/_questions.scss | 1 + app/views/questions/form/_item.html.haml | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/partials/_questions.scss b/app/assets/stylesheets/partials/_questions.scss index ac56af9a2..b9e5ebf43 100644 --- a/app/assets/stylesheets/partials/_questions.scss +++ b/app/assets/stylesheets/partials/_questions.scss @@ -599,6 +599,7 @@ label { flex-shrink: 0; + padding-right: 10px; } input { diff --git a/app/views/questions/form/_item.html.haml b/app/views/questions/form/_item.html.haml index 813da4187..2ff94bc06 100644 --- a/app/views/questions/form/_item.html.haml +++ b/app/views/questions/form/_item.html.haml @@ -8,9 +8,9 @@ %legend#question_images_label = f.label t('questions.form.items.item'), id: 'fs_legend_item' = i.remove_nested_fields_link content_tag(:i, nil, class: 'icon-trash', :'data-tooltip' => t('questions.form.items.remove')) - = link_to content_tag(:i, nil, class: 'icon-comment'), "#void", onclick: 'add_comment(this)', class: 'comment', :'data-tooltip' => t('questions.form.items.comments') - = link_to content_tag(:i, nil, class: 'icon-pictures'), "#void", onclick: 'add_image(this)', class: 'image_icon', :'data-tooltip' => t('questions.form.items.images') - = link_to content_tag(:i, nil, class: 'icon-music'), "#void", onclick: 'add_audio(this)', class: 'audio_icon', :'data-tooltip' => t('questions.form.items.audios') + = link_to content_tag(:i, nil, class: 'icon-comment', :'data-tooltip' => t('questions.form.items.comments')), "#void", onclick: 'add_comment(this)', class: 'comment' + = link_to content_tag(:i, nil, class: 'icon-pictures', :'data-tooltip' => t('questions.form.items.images')), "#void", onclick: 'add_image(this)', class: 'image_icon' + = link_to content_tag(:i, nil, class: 'icon-music', :'data-tooltip' => t('questions.form.items.audios')), "#void", onclick: 'add_audio(this)', class: 'audio_icon' .inline .inputs = i.check_box :value From e4d744e4e21d1138aa10d4e107d3ed4b263dfead Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 10 Dec 2025 14:22:15 -0300 Subject: [PATCH 27/70] refactor: adjust delete button on form question --- app/assets/stylesheets/partials/_questions.scss | 11 ++++++++--- app/views/questions/form/_image.html.haml | 7 ++++++- app/views/questions/form/_media.html.haml | 6 +++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/partials/_questions.scss b/app/assets/stylesheets/partials/_questions.scss index b9e5ebf43..b3f79abfe 100644 --- a/app/assets/stylesheets/partials/_questions.scss +++ b/app/assets/stylesheets/partials/_questions.scss @@ -436,6 +436,8 @@ #images { .duplicatable_nested_form { + display: flex; + flex-direction: column; border-style: solid; border-color: #e8e8e8; border-radius: 20px; @@ -444,8 +446,8 @@ text-align: center; .remove_nested_fields_link { - margin-right: 10px; - float: right; + text-align: right; + margin: 5px 15px; } .input.text.optional { @@ -514,6 +516,9 @@ #audios { .duplicatable_nested_form { + display: flex; + flex-direction: column; + border-style: solid; border-color: #e8e8e8; border-radius: 20px; @@ -833,7 +838,7 @@ } } -.labels { +.text_labels { #question_text { //width: 500px; diff --git a/app/views/questions/form/_image.html.haml b/app/views/questions/form/_image.html.haml index 1f8dc1f3a..728405c4b 100644 --- a/app/views/questions/form/_image.html.haml +++ b/app/views/questions/form/_image.html.haml @@ -5,6 +5,7 @@ - if (f.object.new_record? || img.object.persisted? || @count == count_images + 1) - @count += 1 .duplicatable_nested_form + = img.remove_nested_fields_link content_tag(:i, nil, class: 'icon-trash') .upload-preview - if img.object.image.blank? %img{ src: '' } @@ -15,7 +16,6 @@ = link_to t('.add_image'), "#void", onclick: 'add_file(this)', class: 'btn btn_main add_file' .info - = img.remove_nested_fields_link content_tag(:i, nil, class: 'icon-trash') = img.input :legend = img.input :img_alt = img.label t('.alt_desc'), class: 'alt' @@ -31,3 +31,8 @@ $(btn).parent().find('input.file').click(); } + function toggle_alt_info(link){ + // $(link).parent().find('.alt_info_text').toggleClass('invisible'); + $(link).siblings().find('.alt_info_text').toggle('invisible'); + } + diff --git a/app/views/questions/form/_media.html.haml b/app/views/questions/form/_media.html.haml index ce9711657..e35d79ca3 100644 --- a/app/views/questions/form/_media.html.haml +++ b/app/views/questions/form/_media.html.haml @@ -22,7 +22,7 @@ = render partial: 'questions/form/audio', locals: { f: f, eq: nil } .input.string = f.add_nested_fields_link :question_audios, t('.add_more_audios'), class: 'btn' -%fieldset.labels +%fieldset.text_labels %legend#associated_text_label = f.label t('.associated_text') #question_text @@ -32,8 +32,8 @@ - disabled = f.object.disabled_option(@exam_question.exam_id) ? true : nil - checked = f.object.checked_option(@exam_question.exam_id) ? false : true - #associated_radio - = f.collection_radio_buttons :question_text_id, [[true, t('questions.form.text.import')], [false, t('questions.form.text.new')]], :first, :last, checked: checked, disabled: disabled, boolean_style: :inline, item_wrapper_tag: false + #associated_radio + = f.collection_radio_buttons :question_text_id, [[true, t('questions.form.text.import')], [false, t('questions.form.text.new')]], :first, :last, checked: checked, disabled: disabled, boolean_style: :inline, item_wrapper_tag: false = render partial: 'questions/form/text', locals: { f: f, eq: nil } From b840dbd9fea3abbc9a60b7bd6d9e9965a672c78d Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Thu, 11 Dec 2025 12:40:51 -0300 Subject: [PATCH 28/70] refactor: adjust _image.html.haml layout --- .../stylesheets/partials/_questions.scss | 33 +++++++------------ .../stylesheets/partials/_responsive.scss | 7 ++++ app/views/questions/form/_image.html.haml | 13 +++++--- app/views/questions/form/_media.html.haml | 20 ++++++++++- config/locales/en_US.yml | 3 +- config/locales/pt_BR.yml | 3 +- 6 files changed, 50 insertions(+), 29 deletions(-) diff --git a/app/assets/stylesheets/partials/_questions.scss b/app/assets/stylesheets/partials/_questions.scss index b3f79abfe..5ba94f7d4 100644 --- a/app/assets/stylesheets/partials/_questions.scss +++ b/app/assets/stylesheets/partials/_questions.scss @@ -452,36 +452,22 @@ .input.text.optional { margin-top: 10px; - - // textarea { - // width: 300px; - // } } } - textarea { - height: 60px; - } - input[type='text'] { - // width: 294px; margin-top: 5px; + width: 50%; } .info { - // text-align: right; - display: inline-block; + display: flex; + flex-direction: column; vertical-align: top; text-align: center; - - input { - margin-top: 5px; - } + margin: 0 15px; label.alt { - // width: 300px; - // text-align: left; - // margin-top: -5px; display: inline; } } @@ -492,10 +478,7 @@ } .upload-preview { - // width: 190px; - // display: inline-block; text-align: center; - // margin-left: 24px; display: flex; flex-direction: column; @@ -594,6 +577,14 @@ } } + .alt_info { + margin-top: 10px; + + label { + margin-left: 5px; + } + } + .labels { display: flex; flex-direction: row; diff --git a/app/assets/stylesheets/partials/_responsive.scss b/app/assets/stylesheets/partials/_responsive.scss index 2ce4c96ed..0a181fe38 100644 --- a/app/assets/stylesheets/partials/_responsive.scss +++ b/app/assets/stylesheets/partials/_responsive.scss @@ -731,6 +731,13 @@ text-align: left; } + .question_question_images_legend .form_label, + .question_question_images_img_alt .form_label, + .question_question_audios_description .form_label, + label.text.optional.control-label.form_label { + display: inline-block; + } + label { display: block; } diff --git a/app/views/questions/form/_image.html.haml b/app/views/questions/form/_image.html.haml index 728405c4b..027af754a 100644 --- a/app/views/questions/form/_image.html.haml +++ b/app/views/questions/form/_image.html.haml @@ -18,7 +18,14 @@ .info = img.input :legend = img.input :img_alt - = img.label t('.alt_desc'), class: 'alt' + + .alt_info.title + = link_to "#void", onclick: 'toggle_alt_info(this)', onkeydown: 'click_on_keypress(event, this);' do + =raw t('.alt_title') + %i.icon-arrow-down-triangle + %i.icon-arrow-up-triangle.invisible + .invisible.alt_info_text{tabindex: 0} + = img.label t('.alt_desc'), class: 'alt' -else - @count += 1 @@ -31,8 +38,4 @@ $(btn).parent().find('input.file').click(); } - function toggle_alt_info(link){ - // $(link).parent().find('.alt_info_text').toggleClass('invisible'); - $(link).siblings().find('.alt_info_text').toggle('invisible'); - } diff --git a/app/views/questions/form/_media.html.haml b/app/views/questions/form/_media.html.haml index e35d79ca3..9b85cea2f 100644 --- a/app/views/questions/form/_media.html.haml +++ b/app/views/questions/form/_media.html.haml @@ -60,4 +60,22 @@ $('#import').show(); }); - }) \ No newline at end of file + }) + + function toggle_alt_info(link){ + var parent = $(link).parent(); + parent.siblings().focus(); + var information = parent.siblings('.alt_info_text') + + if(information.hasClass('invisible')){ + information.removeClass('invisible') + parent.find('i.icon-arrow-up-triangle').removeClass('invisible'); + parent.find('i.icon-arrow-down-triangle').addClass('invisible'); + }else{ + information.addClass('invisible') + parent.find('i.icon-arrow-up-triangle').addClass('invisible'); + parent.find('i.icon-arrow-down-triangle').removeClass('invisible'); + } + + $(link).focus(); + } \ No newline at end of file diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index 6e1263c6d..bbf319031 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -4597,7 +4597,8 @@ en_US: add_labels: Add labels add_audios: Add audios image: - alt_desc: 'Alt: Mandatory description to accessibility (disabled people)' + alt_title: 'What is Alt?' + alt_desc: 'Mandatory description to accessibility (disabled people)' add_image: Add/update image audio: add_audio: Add/update audio diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index 9803b153f..793482c63 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -4922,7 +4922,8 @@ pt_BR: add_labels: Adicionar palavras-chave add_audios: Adicionar áudios image: - alt_desc: 'Alt: Descrição obrigatória para acessibilidade (pessoas com deficiência)' + alt_title: 'O que é o Alt?' + alt_desc: 'Descrição obrigatória para acessibilidade (pessoas com deficiência)' add_image: Adicionar/alterar imagem audio: add_audio: Adicionar/alterar áudio From 2df0cb76f2da05d164a71e76e571782256884c70 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Thu, 11 Dec 2025 12:41:56 -0300 Subject: [PATCH 29/70] refactor: adjust _audio.html.haml layout --- .../stylesheets/partials/_questions.scss | 21 +++++++++---------- app/views/questions/form/_audio.html.haml | 11 ++++++++-- config/locales/en_US.yml | 3 ++- config/locales/pt_BR.yml | 3 ++- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/app/assets/stylesheets/partials/_questions.scss b/app/assets/stylesheets/partials/_questions.scss index 5ba94f7d4..99ac00d54 100644 --- a/app/assets/stylesheets/partials/_questions.scss +++ b/app/assets/stylesheets/partials/_questions.scss @@ -510,8 +510,8 @@ text-align: center; .remove_nested_fields_link { - float: right; - margin-right: 10px; + text-align: right; + margin: 5px 15px; } .input.text.optional { @@ -520,20 +520,21 @@ } textarea { - // width: 300px; - width: 85%; - height: 30px; + width: 50%; + height: 70px; } input[type='text'] { - // width: 294px; margin-top: 5px; + width: 50%; } .info { - // text-align: right; - display: inline-block; + display: flex; + flex-direction: column; vertical-align: top; + text-align: center; + margin: 0 15px; input { margin-top: 5px; @@ -541,10 +542,8 @@ label.alt, .exam_question_question_question_audios_audio_description label { - // width: 300px; - // text-align: left; - // margin-top: -5px; display: inline; + margin: 0 10px; } } diff --git a/app/views/questions/form/_audio.html.haml b/app/views/questions/form/_audio.html.haml index e99abee3d..26f09c2aa 100644 --- a/app/views/questions/form/_audio.html.haml +++ b/app/views/questions/form/_audio.html.haml @@ -5,6 +5,7 @@ - if (f.object.new_record? || aud.object.persisted? || @count == count_audios + 1) - @count += 1 .duplicatable_nested_form + = aud.remove_nested_fields_link content_tag(:i, nil, class: 'icon-trash') .upload-preview - if aud.object.audio.blank? %audio{ src: '' } @@ -16,10 +17,16 @@ = link_to t('.add_audio'), "#void", onclick: 'add_file(this)', class: 'btn btn_main add_file' .info - = aud.remove_nested_fields_link content_tag(:i, nil, class: 'icon-trash') = aud.input :audio_description, as: :text = aud.input :description - = aud.label t('.aud_desc'), class: 'alt' + + .alt_info.title + = link_to "#void", onclick: 'toggle_alt_info(this)', onkeydown: 'click_on_keypress(event, this);' do + =raw t('.alt_title') + %i.icon-arrow-down-triangle + %i.icon-arrow-up-triangle.invisible + .invisible.alt_info_text{tabindex: 0} + = aud.label t('.aud_desc'), class: 'alt' -else - @count += 1 diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index bbf319031..bbdd7c980 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -4602,7 +4602,8 @@ en_US: add_image: Add/update image audio: add_audio: Add/update audio - aud_desc: 'Alt: Mandatory description to accessibility (disabled people)' + alt_title: 'Why use Audio Description?' + aud_desc: 'If there are any hearing-impaired students in the class, please provide audio description so that they can follow the content.' text: text: Text associated_text: Associated text diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index 793482c63..b02f1ac80 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -4927,7 +4927,8 @@ pt_BR: add_image: Adicionar/alterar imagem audio: add_audio: Adicionar/alterar áudio - aud_desc: 'Se houver algum deficiente auditivo na turma, por favor realizar a áudio descrição de modo que este aluno não seja prejudicado.' + alt_title: 'Por que usar a Áudio Descrição?' + aud_desc: 'Se houver algum deficiente auditivo na turma, por favor realizar a áudio descrição para que o aluno possa acompanhar o conteúdo.' text: text: Texto associated_text: Texto associado From 02ae2bbcdafca0af1cdf9bd64cb01b933e9ade2a Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Thu, 11 Dec 2025 15:43:24 -0300 Subject: [PATCH 30/70] chore: minor layout corrections --- app/assets/stylesheets/partials/_questions.scss | 3 ++- config/locales/pt_BR.yml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/partials/_questions.scss b/app/assets/stylesheets/partials/_questions.scss index 99ac00d54..eb45ddb9e 100644 --- a/app/assets/stylesheets/partials/_questions.scss +++ b/app/assets/stylesheets/partials/_questions.scss @@ -1,5 +1,6 @@ .questions, -[class^='list_questions_exam_'] { +[class^='list_questions_exam_'], +.exam_questions { .search { label { word-wrap: break-word; diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index b02f1ac80..1f0a15d36 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -4846,7 +4846,7 @@ pt_BR: score: Pontuação associated_text: Texto associado notification: "Houve uma alteração na questão com o enunciado '%{question_enunciation}'. " - notify_editors_alert: "Esta questão está em um rascunho de prova, ou seja, em uma prova que ainda não foi iniciada ou respondida. Quando for modificada, os editores da disciplina em que a prova será aplicada serão notificados. Deseja continuar?" + notify_editors_alert: "Esta questão está em um rascunho de prova, ou em uma prova que ainda não foi iniciada ou respondida. Quando for modificada, os editores da disciplina em que a prova será aplicada serão notificados. Deseja continuar?" question: preview: Visualização public: Pública From 13169f71bb580044930225217499ce3a66542f9b Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Mon, 15 Dec 2025 09:39:13 -0300 Subject: [PATCH 31/70] feat: create verify_edit_permissions route --- app/assets/javascripts/questions.js.erb | 16 ++++++++-------- app/controllers/exam_questions_controller.rb | 9 +++++++++ app/controllers/questions_controller.rb | 4 +--- app/models/exam.rb | 3 +++ app/models/exam_question.rb | 16 ++++++++++++++++ app/models/question.rb | 5 +++-- config/routes.rb | 3 ++- 7 files changed, 42 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index c52908afc..c2c0fe1be 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -191,19 +191,21 @@ function edit_question(icon, replace_list){ return false; } + var verify_url = ""; + if(replace_list != '.list_exams'){ - var questions_ids = $('.ckb_question:checked', $(icon).parents("div.list_questions")).map(function() { return this.value; }).get(); - var ids = questions_ids; + var ids = $('.ckb_question:checked', $(icon).parents("div.list_questions")).map(function() { return this.value; }).get(); + + verify_url = "<%=Rails.application.routes.url_helpers.verify_edit_permissions_question_path(':id')%>".replace(':id', ids); }else{ var ids = $('.ckb_question:checked', $(icon).parents("[id^='exam_']")).map(function() { return this.value; }).get(); - var questions_ids = $('.ckb_question:checked', $(icon).parents("[id^='exam_']")).map(function() { return $(this).data('question-id'); }).get(); - var exam_id = $('.ckb_question:checked', $(icon).parents("[id^='exam_']")).map(function() { return $(this).data('exam-id'); }).get(); + + verify_url = "<%=Rails.application.routes.url_helpers.verify_edit_permissions_exam_question_path(':id')%>".replace(':id', ids); } if (!(!ids.length || ids.length > 1)) { - var base_verify_url = "<%=Rails.application.routes.url_helpers.verify_edit_permissions_question_path(':id')%>".replace(':id', questions_ids); - $.get(exam_id ? (base_verify_url + `/${exam_id}`) : base_verify_url, function(data){ + $.get(verify_url, function(data){ if (data.action === 'copy'){ if (!confirm(data.alert)) @@ -228,7 +230,6 @@ function edit_question(icon, replace_list){ return this; } - $(icon).call_fancybox({ href : url_edit, open: true, @@ -236,7 +237,6 @@ function edit_question(icon, replace_list){ 'autoSize': false, width: '700', height: 'auto', - //maxHeight: '70%', beforeClose: function() { clear_ckeditor(); } }); } diff --git a/app/controllers/exam_questions_controller.rb b/app/controllers/exam_questions_controller.rb index bf274e408..f91698ff0 100644 --- a/app/controllers/exam_questions_controller.rb +++ b/app/controllers/exam_questions_controller.rb @@ -111,6 +111,15 @@ def update render_json_error(error, 'questions.error') end + def verify_edit_permissions + Rails.logger.info "\n\n\n Entrei no verify_edit_permissions do exam_question\n\n\n" + + exam_question = ExamQuestion.find(params[:id]) + + edition_status = exam_question.verify_edit_permission(current_user) + + end + def remove_file_item authorize! :update, Question diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index f0b761849..9f821ca66 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -121,9 +121,8 @@ def update def verify_edit_permissions question = Question.find params[:id] - exam_id = params[:exam_id] - edition_status = question.verify_edit_permission(exam_id, current_user) + edition_status = question.verify_edit_permission(current_user) if [:edit_full, :edit_and_notify].include?(edition_status) authorize! :update, Question @@ -131,7 +130,6 @@ def verify_edit_permissions else alert_key = edition_status == :copy_blocked_exam ? 'in_use_question' : 'not_owner' render json: { action: 'copy', alert: I18n.t(alert_key, scope: [:questions, :error]) }, status: :ok - end rescue RuntimeError => error diff --git a/app/models/exam.rb b/app/models/exam.rb index 5d13ebea9..8042e4230 100644 --- a/app/models/exam.rb +++ b/app/models/exam.rb @@ -498,6 +498,9 @@ def get_duration(total_time) # Return true if the exam is in draft mode, has not started, or started without ACU, and the user is the editor of the class where the exam is being used. # Return false if exam is published and closed, or started with ACU, or started without ACU and user is not a class editor. def can_questions_be_edited?(current_user, is_exam_question = false) + Rails.logger.info "\n\n\n Current user id: #{current_user.id}\n\n\n" + Rails.logger.info "\n\n\n is_exam_question: #{is_exam_question}\n\n\n" + has_acu = academic_allocation_users.any? allocation_tags = academic_allocations.pluck(:allocation_tag_id) is_editor = is_exam_question ? true : current_user.profiles_with_access_on('update', 'questions', allocation_tags, true).any? diff --git a/app/models/exam_question.rb b/app/models/exam_question.rb index 63d9cde1a..d553be83d 100644 --- a/app/models/exam_question.rb +++ b/app/models/exam_question.rb @@ -139,4 +139,20 @@ def each_item_score I18n.t("questions.question.points", score: item_score) end + def verify_edit_permission(current_user) + question = Question.find(question_id) + + + has_blocking_exam = question.exams.any? do |exam| + next false if exam.nil? + !exam.can_questions_be_edited?(current_user, exam.id == exam_id) + end + + if exams.ids.length > 1 + + else + return :edit_and_notify if question.owner? + end + end + end diff --git a/app/models/question.rb b/app/models/question.rb index 17df8027f..1ea13fdae 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -249,10 +249,11 @@ def have_access? # Return true if question can be directly edited # Return fase if question must be copied to be edited # Raise an exception if the question cannot be edited - def verify_edit_permission(exam_id = nil, current_user) + def verify_edit_permission(current_user) + has_blocking_exam = exams.any? do |exam| next false if exam.nil? - !exam.can_questions_be_edited?(current_user, !exam_id.nil?) + !exam.can_questions_be_edited?(current_user) end raise 'in_use' if !owner? && privacy diff --git a/config/routes.rb b/config/routes.rb index 17f7d2723..4229bebb1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -693,7 +693,7 @@ member do put :change_status put :publish, action: :change_status, status: true - get 'verify_edit_permissions(/:exam_id)', as: :verify_edit_permissions, action: :verify_edit_permissions + get :verify_edit_permissions get :copy_verify_owners, action: :verify_owners, copy: true get :show_verify_owners, action: :verify_owners, show: true get :copy @@ -705,6 +705,7 @@ put "order/:change_id", action: :order, as: :change_order put :annul get '/export/exam_questions/steps', to: 'exam_questions#export_steps', as: :export_steps + get :verify_edit_permissions put :publish get :copy put :remove_image_item, to: 'exam_questions#remove_file_item', type: 'image' From 755fb37a5895c968d21be4223f92fb82cef2a06e Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Mon, 15 Dec 2025 15:43:13 -0300 Subject: [PATCH 32/70] feat: adjust verify_edit_permission to include exam_question params --- app/models/exam.rb | 3 --- app/models/exam_question.rb | 16 ---------------- app/models/question.rb | 28 ++++++++++++++++++++++------ 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/app/models/exam.rb b/app/models/exam.rb index 8042e4230..5d13ebea9 100644 --- a/app/models/exam.rb +++ b/app/models/exam.rb @@ -498,9 +498,6 @@ def get_duration(total_time) # Return true if the exam is in draft mode, has not started, or started without ACU, and the user is the editor of the class where the exam is being used. # Return false if exam is published and closed, or started with ACU, or started without ACU and user is not a class editor. def can_questions_be_edited?(current_user, is_exam_question = false) - Rails.logger.info "\n\n\n Current user id: #{current_user.id}\n\n\n" - Rails.logger.info "\n\n\n is_exam_question: #{is_exam_question}\n\n\n" - has_acu = academic_allocation_users.any? allocation_tags = academic_allocations.pluck(:allocation_tag_id) is_editor = is_exam_question ? true : current_user.profiles_with_access_on('update', 'questions', allocation_tags, true).any? diff --git a/app/models/exam_question.rb b/app/models/exam_question.rb index d553be83d..63d9cde1a 100644 --- a/app/models/exam_question.rb +++ b/app/models/exam_question.rb @@ -139,20 +139,4 @@ def each_item_score I18n.t("questions.question.points", score: item_score) end - def verify_edit_permission(current_user) - question = Question.find(question_id) - - - has_blocking_exam = question.exams.any? do |exam| - next false if exam.nil? - !exam.can_questions_be_edited?(current_user, exam.id == exam_id) - end - - if exams.ids.length > 1 - - else - return :edit_and_notify if question.owner? - end - end - end diff --git a/app/models/question.rb b/app/models/question.rb index 1ea13fdae..d2f784d74 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -249,18 +249,34 @@ def have_access? # Return true if question can be directly edited # Return fase if question must be copied to be edited # Raise an exception if the question cannot be edited - def verify_edit_permission(current_user) + def verify_edit_permission(current_user, exam_id=nil) + Rails.logger.info "\n\n\n Current user: #{current_user} \n\n\n" has_blocking_exam = exams.any? do |exam| next false if exam.nil? - !exam.can_questions_be_edited?(current_user) + !exam.can_questions_be_edited?(current_user, exam.id == exam_id) end - raise 'in_use' if !owner? && privacy + if exam_id.nil? + raise 'in_use' if !owner? && privacy - return :copy_blocked_exam if has_blocking_exam - return :copy_ownership unless owner? - exams.any? ? :edit_and_notify : :edit_full + return :copy_blocked_exam if has_blocking_exam + return :copy_ownership unless owner? + exams.any? ? :edit_and_notify : :edit_full + else + has_access = owner? || !privacy + n_exams = exams.ids.length > 1 + + if has_blocking_exam + return :copy_repository if n_exams || has_access + raise 'in_use' + end + + return (has_access ? :fork_copy : :only_points) if n_exams + return :edit_and_notify if owner? + return :only_points if privacy + :can_fork_copy + end end def owner?(s = false) From 7215d35c5694844e347df6738ab912bb246f875e Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Mon, 15 Dec 2025 15:44:22 -0300 Subject: [PATCH 33/70] feat: implement edit permissions on controller e js file --- app/assets/javascripts/questions.js.erb | 28 +++++++++++++++++--- app/controllers/exam_questions_controller.rb | 19 ++++++++++--- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index c2c0fe1be..7b545b413 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -206,12 +206,32 @@ function edit_question(icon, replace_list){ if (!(!ids.length || ids.length > 1)) { $.get(verify_url, function(data){ - if (data.action === 'copy'){ - if (!confirm(data.alert)) - return this; + var action = data.action; + + if (action === 'can_fork_copy') { + if (confirm("Deseja alterar somente a pontuação da questão selecionada na prova? Clique em 'Ok' para confirmar e 'Cancelar' para continuar com a edição.")) { + action = 'points_only'; + } + } + + if (action === 'copy' || action === 'can_fork_copy' || action === 'fork_copy' || action === 'copy_repository') { + + if (action === 'copy' || action === 'copy_repository') { + if (!confirm(data.alert)) + return this; + } + + var url_copy = ''; + + if (action === 'copy_repository') { + url_copy = "<%=Rails.application.routes.url_helpers.copy_question_path(':id')%>".replace(':id', ids); + } else { + url_copy = $(icon).prev().data('link-to-copy').replace(':id', ids) + "?fork_copy"; + } + + console.log(url_copy); - var url_copy = $(icon).prev().data('link-to-copy').replace(':id', ids); $(icon).prev().call_fancybox({ href : url_copy, open: true, maxHeight: '70%', 'autoDimensions': false, 'autoSize': false, width: '700', height: 'auto', }); if(replace_list != ''){ $.get($(replace_list).data("link-list"), function(data2){ diff --git a/app/controllers/exam_questions_controller.rb b/app/controllers/exam_questions_controller.rb index f91698ff0..c280aefb1 100644 --- a/app/controllers/exam_questions_controller.rb +++ b/app/controllers/exam_questions_controller.rb @@ -112,11 +112,24 @@ def update end def verify_edit_permissions - Rails.logger.info "\n\n\n Entrei no verify_edit_permissions do exam_question\n\n\n" - exam_question = ExamQuestion.find(params[:id]) + question = Question.find(exam_question.question_id) + + edition_status = question.verify_edit_permission( current_user, exam_question.exam_id ) + + Rails.logger.info "\n\n\n edition_status: #{edition_status} \n\n\n" - edition_status = exam_question.verify_edit_permission(current_user) + if [:edit_and_notify].include?(edition_status) + render json: { action: 'edit', notify: true }, status: :ok + else + alert = '' + if edition_status == :copy_repository + alert = "Está questão está em uma prova já iniciada. Deseja criar uma cópia no repositório?" + elsif edition_status == :only_points + alert = "Você não tem autorização para editar o conteúdo desta questão. Deseja alterar apenas a pontuação atribuída a ela nesta prova?" + end + render json: { action: edition_status, alert: alert }, status: :ok + end end From b4a2b98ccfc271307f5f177e51783d85e52ea833 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Tue, 16 Dec 2025 09:29:59 -0300 Subject: [PATCH 34/70] refactor: adjust question_controller veryfi_edit_permissions function to include exam_questions validations and remove unused route --- app/controllers/exam_questions_controller.rb | 22 ----------------- app/controllers/questions_controller.rb | 26 +++++++++++++++++--- config/routes.rb | 3 +-- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/app/controllers/exam_questions_controller.rb b/app/controllers/exam_questions_controller.rb index c280aefb1..bf274e408 100644 --- a/app/controllers/exam_questions_controller.rb +++ b/app/controllers/exam_questions_controller.rb @@ -111,28 +111,6 @@ def update render_json_error(error, 'questions.error') end - def verify_edit_permissions - exam_question = ExamQuestion.find(params[:id]) - question = Question.find(exam_question.question_id) - - edition_status = question.verify_edit_permission( current_user, exam_question.exam_id ) - - Rails.logger.info "\n\n\n edition_status: #{edition_status} \n\n\n" - - if [:edit_and_notify].include?(edition_status) - render json: { action: 'edit', notify: true }, status: :ok - else - alert = '' - if edition_status == :copy_repository - alert = "Está questão está em uma prova já iniciada. Deseja criar uma cópia no repositório?" - elsif edition_status == :only_points - alert = "Você não tem autorização para editar o conteúdo desta questão. Deseja alterar apenas a pontuação atribuída a ela nesta prova?" - end - render json: { action: edition_status, alert: alert }, status: :ok - end - - end - def remove_file_item authorize! :update, Question diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 9f821ca66..8b51cd9b0 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -121,15 +121,16 @@ def update def verify_edit_permissions question = Question.find params[:id] + exam_id = params[:exam_id] - edition_status = question.verify_edit_permission(current_user) + edition_status = question.verify_edit_permission(current_user, exam_id) if [:edit_full, :edit_and_notify].include?(edition_status) authorize! :update, Question - render json: { action: 'edit', notify: edition_status == :edit_and_notify, }, status: :ok + render json: { action: 'edit', notify: edition_status == :edit_and_notify }, status: :ok else - alert_key = edition_status == :copy_blocked_exam ? 'in_use_question' : 'not_owner' - render json: { action: 'copy', alert: I18n.t(alert_key, scope: [:questions, :error]) }, status: :ok + repository_action = edition_status == (:copy_blocked_exam || :copy_ownership) ? :copy : edition_status + render json: { action: repository_action, alert: I18n.t(get_edit_permissions_alert_key(edition_status), scope: [:questions, :error]) }, status: :ok end rescue RuntimeError => error @@ -219,4 +220,21 @@ def log(object, message, type=LogAction::TYPE[:update]) LogAction.create(params_to_log.merge!(description: message, log_type: type)) end + def get_edit_permissions_alert_key(status) + case status + when :copy_blocked_exam + 'in_use_question' + when :copy_ownership + 'not_owner' + when :copy_repository + 'copy_repository' + when :only_points + 'only_points' + when :can_fork_copy + 'can_fork_copy' + else + 'in_use' + end + end + end diff --git a/config/routes.rb b/config/routes.rb index 4229bebb1..17f7d2723 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -693,7 +693,7 @@ member do put :change_status put :publish, action: :change_status, status: true - get :verify_edit_permissions + get 'verify_edit_permissions(/:exam_id)', as: :verify_edit_permissions, action: :verify_edit_permissions get :copy_verify_owners, action: :verify_owners, copy: true get :show_verify_owners, action: :verify_owners, show: true get :copy @@ -705,7 +705,6 @@ put "order/:change_id", action: :order, as: :change_order put :annul get '/export/exam_questions/steps', to: 'exam_questions#export_steps', as: :export_steps - get :verify_edit_permissions put :publish get :copy put :remove_image_item, to: 'exam_questions#remove_file_item', type: 'image' From b6f470c227636bf0af623a923ecde4f0cf2ac33a Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Tue, 16 Dec 2025 09:31:07 -0300 Subject: [PATCH 35/70] chore: add alert messages to internationalization --- config/locales/en_US.yml | 3 +++ config/locales/pt_BR.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index bbdd7c980..b78697eaa 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -4622,6 +4622,9 @@ en_US: in_use_question: "This question is being used on a started test. Would you like to duplicate it for editing?" in_use_exam_question: "This question is being used on another exam. Would you like to duplicate it for editing?" not_owner: "Only the author can edit the question. Do you want to make a copy?" + copy_repository: "This question is in a test that has already begun. Do you want to create a copy in the repository?" + only_points: "You are not authorized to edit the content of this question. Do you want to change the score of the selected question on the exam?" + can_fork_copy: "Do you want to change the score of the selected question on the exam? Click 'OK' to confirm and 'Cancel' to continue editing." privacy_in_use: "can't be defined as private, because is being used at exams. If you're the author and the question is in use only at your exams, we suggest that you remove it from the exam and change it at Repositories area. If the question is in use at others exams that are not yours, it's not possible to change it." privacy_permission: can't be defined as private. You don't have permission to execute this action. min_items: The question must have, at least, 3 items diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index 1f0a15d36..ea0fb4c40 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -4947,6 +4947,9 @@ pt_BR: in_use_question: "Essa questão está sendo utilizada em uma prova já iniciada. Deseja duplicá-la para fazer a edição?" in_use_exam_question: "Essa questão está sendo utilizada em outra prova. Deseja duplicá-la para fazer a edição?" not_owner: "Somente o autor pode editar a questão. Deseja fazer uma cópia?" + copy_repository: "Está questão está em uma prova já iniciada. Deseja criar uma cópia no repositório?" + only_points: "Você não tem autorização para editar o conteúdo desta questão. Deseja alterar a pontuação da questão selecionada na prova?" + can_fork_copy: "Deseja alterar a pontuação da questão selecionada na prova? Clique em 'Ok' para confirmar e 'Cancelar' para continuar com a edição." privacy_in_use: "não pode ser definida como privada, pois está sendo usada em provas. Se você é o autor e a questão está em uso apenas em sua prova, sugerimos que remova-a de sua prova e se dirija ao repositório para alterá-la. Caso a questão esteja em uso em outras provas não criadas por você, não é possível alterá-la" privacy_permission: não pode ser definida como privada. Você não tem permissão para realizar esta ação. min_items: A questão precisa ter, ao menos, 3 itens From 0e02e25f2454935c852081adf96035a9dea14469 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Tue, 16 Dec 2025 12:03:55 -0300 Subject: [PATCH 36/70] chore: adjust edit_question function on js --- app/assets/javascripts/questions.js.erb | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index 7b545b413..a858ffa58 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -191,23 +191,22 @@ function edit_question(icon, replace_list){ return false; } - var verify_url = ""; + var questions_ids; + var ids; if(replace_list != '.list_exams'){ - var ids = $('.ckb_question:checked', $(icon).parents("div.list_questions")).map(function() { return this.value; }).get(); - - verify_url = "<%=Rails.application.routes.url_helpers.verify_edit_permissions_question_path(':id')%>".replace(':id', ids); + questions_ids = $('.ckb_question:checked', $(icon).parents("div.list_questions")).map(function() { return this.value; }).get(); + ids = questions_ids }else{ - var ids = $('.ckb_question:checked', $(icon).parents("[id^='exam_']")).map(function() { return this.value; }).get(); - - verify_url = "<%=Rails.application.routes.url_helpers.verify_edit_permissions_exam_question_path(':id')%>".replace(':id', ids); + ids = $('.ckb_question:checked', $(icon).parents("[id^='exam_']")).map(function() { return this.value; }).get(); + questions_ids = $('.ckb_question:checked', $(icon).parents("[id^='exam_']")).map(function() { return $(this).data('question-id'); }).get(); + var exam_id = $('.ckb_question:checked', $(icon).parents("[id^='exam_']")).map(function() { return $(this).data('exam-id'); }).get(); } if (!(!ids.length || ids.length > 1)) { + var base_verify_url = "<%=Rails.application.routes.url_helpers.verify_edit_permissions_question_path(':id')%>".replace(':id', questions_ids); - $.get(verify_url, function(data){ - - var action = data.action; + $.get(exam_id ? (base_verify_url + `/${exam_id}`) : base_verify_url, function(data){ if (action === 'can_fork_copy') { if (confirm("Deseja alterar somente a pontuação da questão selecionada na prova? Clique em 'Ok' para confirmar e 'Cancelar' para continuar com a edição.")) { @@ -230,8 +229,6 @@ function edit_question(icon, replace_list){ url_copy = $(icon).prev().data('link-to-copy').replace(':id', ids) + "?fork_copy"; } - console.log(url_copy); - $(icon).prev().call_fancybox({ href : url_copy, open: true, maxHeight: '70%', 'autoDimensions': false, 'autoSize': false, width: '700', height: 'auto', }); if(replace_list != ''){ $.get($(replace_list).data("link-list"), function(data2){ From ce412ab7e014d9d34dd09fb07b6186c40a545704 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Tue, 16 Dec 2025 12:06:08 -0300 Subject: [PATCH 37/70] feat: add fork_copy and copy_repository logic --- app/assets/javascripts/questions.js.erb | 17 +++++++++-------- app/controllers/exam_questions_controller.rb | 5 +++-- app/models/exam_question.rb | 10 ++++++++-- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index a858ffa58..e9c4b4192 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -208,25 +208,26 @@ function edit_question(icon, replace_list){ $.get(exam_id ? (base_verify_url + `/${exam_id}`) : base_verify_url, function(data){ - if (action === 'can_fork_copy') { - if (confirm("Deseja alterar somente a pontuação da questão selecionada na prova? Clique em 'Ok' para confirmar e 'Cancelar' para continuar com a edição.")) { - action = 'points_only'; + if (data.action === 'can_fork_copy') { + if (confirm(data.alert)) { + data.action = 'only_points'; + } else { + data.action = 'fork_copy'; } } - if (action === 'copy' || action === 'can_fork_copy' || action === 'fork_copy' || action === 'copy_repository') { - - if (action === 'copy' || action === 'copy_repository') { + if (['fork_copy', 'copy_repository', 'copy'].includes(data.action)) { + if (data.action != 'fork_copy'){ if (!confirm(data.alert)) return this; } var url_copy = ''; - if (action === 'copy_repository') { + if (data.action === 'copy_repository') { url_copy = "<%=Rails.application.routes.url_helpers.copy_question_path(':id')%>".replace(':id', ids); } else { - url_copy = $(icon).prev().data('link-to-copy').replace(':id', ids) + "?fork_copy"; + url_copy = $(icon).prev().data('link-to-copy').replace(':id', ids) + (data.action === 'fork_copy' ? "?fork_copy=true" : ""); } $(icon).prev().call_fancybox({ href : url_copy, open: true, maxHeight: '70%', 'autoDimensions': false, 'autoSize': false, width: '700', height: 'auto', }); diff --git a/app/controllers/exam_questions_controller.rb b/app/controllers/exam_questions_controller.rb index bf274e408..4545e6c05 100644 --- a/app/controllers/exam_questions_controller.rb +++ b/app/controllers/exam_questions_controller.rb @@ -360,11 +360,12 @@ def export def copy authorize! :copy, Question exam_question = ExamQuestion.find params[:id] + is_fork_copy = params[:fork_copy].nil? ? false : true exam_question.can_save? exam_question.question.can_copy? - @exam_question = ExamQuestion.copy(exam_question, current_user.id) + @exam_question = ExamQuestion.copy(exam_question, current_user.id, is_fork_copy) - log(ExamQuestion.exam, "question: #{exam_question.question_id} [copy], #{exam_question.log_description}", LogAction::TYPE[:create]) rescue nil + log(ExamQuestion.exam, "question: #{exam_question.question_id} #{is_fork_copy ? '[fork_copy]' : '[copy]' }, #{exam_question.log_description}", LogAction::TYPE[:create]) rescue nil build_exam_question diff --git a/app/models/exam_question.rb b/app/models/exam_question.rb index 63d9cde1a..84c6a615f 100644 --- a/app/models/exam_question.rb +++ b/app/models/exam_question.rb @@ -24,10 +24,16 @@ def recalculate_grades exam.recalculate_grades(nil,nil,nil,manually=true) end - def self.copy(exam_question_to_copy, user_id = nil) + def self.copy(exam_question_to_copy, user_id = nil, is_fork_copy = false) question = Question.copy(exam_question_to_copy.question, user_id) exam_question = ExamQuestion.create exam_question_to_copy.attributes.except('id', 'question_id', 'annulled').merge({ question_id: question.id, annulled: false }) - exam_question_to_copy.update_attributes annulled: true + + if is_fork_copy + exam_question_to_copy.destroy + else + exam_question_to_copy.update_attributes annulled: true + end + exam_question end From 236649682afe7a0092a41694ae5fd14998490aca Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Tue, 16 Dec 2025 12:07:13 -0300 Subject: [PATCH 38/70] feat: implement only points logic --- app/assets/javascripts/questions.js.erb | 2 +- app/controllers/exam_questions_controller.rb | 5 +++ app/views/exam_questions/_steps.html.haml | 31 +++++++++-------- app/views/exam_questions/edit.html.haml | 2 +- app/views/questions/form/_info.html.haml | 35 +++++++++++--------- 5 files changed, 45 insertions(+), 30 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index e9c4b4192..64de26ed9 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -239,7 +239,7 @@ function edit_question(icon, replace_list){ }); } } else { - var url_edit = $(icon).data('link-to-edit').replace(':id', ids); + var url_edit = $(icon).data('link-to-edit').replace(':id', ids) + (data.action === 'only_points' ? "?only_points=true" : ""); if (data.notify) { url_edit += "?notify_responsibles=true"; diff --git a/app/controllers/exam_questions_controller.rb b/app/controllers/exam_questions_controller.rb index 4545e6c05..6d5530486 100644 --- a/app/controllers/exam_questions_controller.rb +++ b/app/controllers/exam_questions_controller.rb @@ -60,6 +60,8 @@ def create def edit @exam_question = ExamQuestion.find(params[:id]) + @only_points = params[:only_points].nil? ? false : true + Rails.logger.info "\n\n\n #{@only_points}\n\n\n" @question_text = QuestionText.find(@exam_question.question.question_text_id) unless @exam_question.question.question_text_id.blank? build_exam_question end @@ -361,6 +363,9 @@ def copy authorize! :copy, Question exam_question = ExamQuestion.find params[:id] is_fork_copy = params[:fork_copy].nil? ? false : true + + Rails.logger.info "\n\n\n is_fork_copy: #{is_fork_copy}\n\n\n" + exam_question.can_save? exam_question.question.can_copy? @exam_question = ExamQuestion.copy(exam_question, current_user.id, is_fork_copy) diff --git a/app/views/exam_questions/_steps.html.haml b/app/views/exam_questions/_steps.html.haml index 566701d14..d66c01aa1 100644 --- a/app/views/exam_questions/_steps.html.haml +++ b/app/views/exam_questions/_steps.html.haml @@ -8,18 +8,23 @@ #steps %ul %li.info.active - = t('questions.form.steps.info') - .dot.active#dot-info - %li.medias - = t('questions.form.steps.medias') - .dot#dot-medias - %li.items - = t('questions.form.steps.items') - .dot#dot-items + - if only_points + = 'Pontuação' + -else + = t('questions.form.steps.info') + .dot.active#dot-info + - unless only_points + %li.medias + = t('questions.form.steps.medias') + .dot#dot-medias + %li.items + = t('questions.form.steps.items') + .dot#dot-items = eq.simple_fields_for :question do |f| .step-info - = render partial: 'questions/form/info', locals: { f: f, eq: eq } - .step-medias.invisible - = render partial: 'questions/form/media', locals: { f: f, eq: nil } - .step-items.invisible - = render partial: 'questions/form/items', locals: { f: f, eq: eq } + = render partial: 'questions/form/info', locals: { f: f, eq: eq, only_points: only_points } + - unless only_points + .step-medias.invisible + = render partial: 'questions/form/media', locals: { f: f, eq: nil } + .step-items.invisible + = render partial: 'questions/form/items', locals: { f: f, eq: eq } diff --git a/app/views/exam_questions/edit.html.haml b/app/views/exam_questions/edit.html.haml index d7742ca55..0272c5203 100644 --- a/app/views/exam_questions/edit.html.haml +++ b/app/views/exam_questions/edit.html.haml @@ -1,3 +1,3 @@ = javascript_include_tag 'questions' -= render partial: 'exam_questions/steps' += render partial: 'exam_questions/steps', locals: { only_points: @only_points } diff --git a/app/views/questions/form/_info.html.haml b/app/views/questions/form/_info.html.haml index c2fe1f053..518695ca9 100644 --- a/app/views/questions/form/_info.html.haml +++ b/app/views/questions/form/_info.html.haml @@ -1,8 +1,9 @@ = javascript_include_tag 'ckeditor/init' -.question_type - = f.label t('questions.new.privacy') - = f.select :privacy, ([ [t('questions.new.public'), false], [t('questions.new.private'), true] ]), include_blank: false +- unless only_points + .question_type + = f.label t('questions.new.privacy') + = f.select :privacy, ([ [t('questions.new.public'), false], [t('questions.new.private'), true] ]), include_blank: false .question_type = f.label t('questions.new.type_question') @@ -27,24 +28,28 @@ .invisible.multiple_choice_new_calculation = raw(t('questions.types.multiple_choice_new_calculation', score_item: t('questions.question.total_amount_per_items'))) -.labels - = f.label t('questions.questions.labels'), class: 'label_labels' - %div#labels_container - = render partial: 'questions/form/label', locals: { f: f } - .input.string - = f.add_nested_fields_link :question_labels, t('.add_labels'), class: 'btn' +- unless only_points + .labels + = f.label t('questions.questions.labels'), class: 'label_labels' + %div#labels_container + = render partial: 'questions/form/label', locals: { f: f } + .input.string + = f.add_nested_fields_link :question_labels, t('.add_labels'), class: 'btn' -= f.input :enunciation, as: :ckeditor + = f.input :enunciation, as: :ckeditor -= label_tag 'question_enunciation', '', id: 'exam_question_question_enunciation' + = label_tag 'question_enunciation', '', id: 'exam_question_question_enunciation' -.ckb - = f.label t('questions.form.add_media'), id: 'add_media' - = link_to (image_tag "#{f.object.have_media? ? 'released' : 'rejected'}.png"), "#void", onclick: 'change(this)', onkeydown: 'click_on_keypress(event, this)', :'data-tooltip' => t('questions.form.add_media'), id: 'add_media_link' + .ckb + = f.label t('questions.form.add_media'), id: 'add_media' + = link_to (image_tag "#{f.object.have_media? ? 'released' : 'rejected'}.png"), "#void", onclick: 'change(this)', onkeydown: 'click_on_keypress(event, this)', :'data-tooltip' => t('questions.form.add_media'), id: 'add_media_link' .form-actions.right_buttons = button_tag t(:cancel), :type => 'button', :onclick => "jQuery.fancybox.close()", class: 'btn btn_default btn_lightbox', alt: t(:cancel) - = button_tag t('.continue'), :type => 'button', :onclick => "go_to_medias()", class: 'btn btn_main btn_lightbox', alt: t(:save), id: 'go_to' + - if only_points + = button_tag t(:save), :type => 'button', :onclick => "save_new_question(#{eq.nil? ? 0 : eq.object.exam_id}, #{f.object.type_question.nil? ? false : f.object.type_question})", class: 'btn btn_main', style: 'margin-bottom: 6px' + - else + = button_tag t('.continue'), :type => 'button', :onclick => "go_to_medias()", class: 'btn btn_main btn_lightbox', alt: t(:save), id: 'go_to' :javascript From afe747b61b51d67146f63ac9541216e2add65103 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Tue, 16 Dec 2025 12:10:48 -0300 Subject: [PATCH 39/70] refactor: remove console messages --- app/assets/javascripts/questions.js.erb | 1 - app/controllers/exam_questions_controller.rb | 4 ---- app/models/question.rb | 2 -- 3 files changed, 7 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index 64de26ed9..355395f2a 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -243,7 +243,6 @@ function edit_question(icon, replace_list){ if (data.notify) { url_edit += "?notify_responsibles=true"; - if (!confirm("<%=I18n.t('questions.questions.notify_editors_alert')%>")) return this; } diff --git a/app/controllers/exam_questions_controller.rb b/app/controllers/exam_questions_controller.rb index 6d5530486..293613716 100644 --- a/app/controllers/exam_questions_controller.rb +++ b/app/controllers/exam_questions_controller.rb @@ -61,7 +61,6 @@ def create def edit @exam_question = ExamQuestion.find(params[:id]) @only_points = params[:only_points].nil? ? false : true - Rails.logger.info "\n\n\n #{@only_points}\n\n\n" @question_text = QuestionText.find(@exam_question.question.question_text_id) unless @exam_question.question.question_text_id.blank? build_exam_question end @@ -364,8 +363,6 @@ def copy exam_question = ExamQuestion.find params[:id] is_fork_copy = params[:fork_copy].nil? ? false : true - Rails.logger.info "\n\n\n is_fork_copy: #{is_fork_copy}\n\n\n" - exam_question.can_save? exam_question.question.can_copy? @exam_question = ExamQuestion.copy(exam_question, current_user.id, is_fork_copy) @@ -379,7 +376,6 @@ def copy render text: t(:no_permission) rescue => error render text: t("exam_questions.errors.#{error}") - # render_json_error(error, 'exam_questions.errors') end private diff --git a/app/models/question.rb b/app/models/question.rb index d2f784d74..1b93bfa7f 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -250,8 +250,6 @@ def have_access? # Return fase if question must be copied to be edited # Raise an exception if the question cannot be edited def verify_edit_permission(current_user, exam_id=nil) - Rails.logger.info "\n\n\n Current user: #{current_user} \n\n\n" - has_blocking_exam = exams.any? do |exam| next false if exam.nil? !exam.can_questions_be_edited?(current_user, exam.id == exam_id) From 7dd4fab1ae54fa0c302238769a519a9e7d0145cf Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Tue, 16 Dec 2025 12:30:03 -0300 Subject: [PATCH 40/70] doc: adjust verify_edit_permission documentation --- app/models/question.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/models/question.rb b/app/models/question.rb index 1b93bfa7f..eb3fe8f50 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -245,9 +245,13 @@ def have_access? return User.current.profiles_with_access_on('show', 'questions', ats, true, false, true).any? end - - # Return true if question can be directly edited - # Return fase if question must be copied to be edited + # Return :edit_full if question can be fully edited (only in repository) + # Return :edit_and_notify if question can be edited but editors must be notified + # Return :copy_ownership and :copy_blocked_exam if question can only be copied (only in repository) + # Return :copy_repository if question can be copied only in repository and must not be changed in the exam + # Return :fork_copy if question can be copied but must be replaced in the exam + # Return :can_fork_copy if question can be copied and replaced in the exam or if only the points will be edited + # Return :only_points if only the points can be edited # Raise an exception if the question cannot be edited def verify_edit_permission(current_user, exam_id=nil) has_blocking_exam = exams.any? do |exam| From d6e0c41b02c42d358a6bf5f421550b7b0ed7e4c0 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 17 Dec 2025 09:33:38 -0300 Subject: [PATCH 41/70] chore: minor code adjusts --- app/assets/javascripts/questions.js.erb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index 355395f2a..bc90f9b26 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -222,7 +222,7 @@ function edit_question(icon, replace_list){ return this; } - var url_copy = ''; + var url_copy; if (data.action === 'copy_repository') { url_copy = "<%=Rails.application.routes.url_helpers.copy_question_path(':id')%>".replace(':id', ids); @@ -242,9 +242,10 @@ function edit_question(icon, replace_list){ var url_edit = $(icon).data('link-to-edit').replace(':id', ids) + (data.action === 'only_points' ? "?only_points=true" : ""); if (data.notify) { - url_edit += "?notify_responsibles=true"; - if (!confirm("<%=I18n.t('questions.questions.notify_editors_alert')%>")) + if (!confirm("<%=I18n.t('questions.questions.notify_editors_alert')%>")) { return this; + } + url_edit += "?notify_responsibles=true"; } $(icon).call_fancybox({ From 74222c4d987aff456e49e00ad31c8774d7fe380d Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 17 Dec 2025 12:56:17 -0300 Subject: [PATCH 42/70] chore: minor code adjusts --- app/views/questions/edit.html.haml | 2 +- app/views/questions/form/_steps.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/questions/edit.html.haml b/app/views/questions/edit.html.haml index 5c520769b..509ad418c 100644 --- a/app/views/questions/edit.html.haml +++ b/app/views/questions/edit.html.haml @@ -1 +1 @@ -= render partial: 'questions/form/steps' \ No newline at end of file += render partial: 'questions/form/steps', locals: { only_points: false } \ No newline at end of file diff --git a/app/views/questions/form/_steps.html.haml b/app/views/questions/form/_steps.html.haml index 9591d37e9..84dbbba9f 100644 --- a/app/views/questions/form/_steps.html.haml +++ b/app/views/questions/form/_steps.html.haml @@ -17,7 +17,7 @@ = t('questions.form.steps.items') .dot#dot-items .step-info - = render partial: 'questions/form/info', locals: { f: f, eq: nil } + = render partial: 'questions/form/info', locals: { f: f, eq: nil, only_points: false } .step-medias.invisible = render partial: 'questions/form/media', locals: { f: f, eq: nil } .step-items.invisible From 2a13e059b82141c109fcad575a07e8356f85b5c2 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Thu, 18 Dec 2025 08:15:30 -0300 Subject: [PATCH 43/70] fix: solve render form error --- app/views/exam_questions/new.html.haml | 2 +- app/views/questions/edit.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/exam_questions/new.html.haml b/app/views/exam_questions/new.html.haml index d7742ca55..f967078ca 100644 --- a/app/views/exam_questions/new.html.haml +++ b/app/views/exam_questions/new.html.haml @@ -1,3 +1,3 @@ = javascript_include_tag 'questions' -= render partial: 'exam_questions/steps' += render partial: 'exam_questions/steps', locals: { only_points: false } diff --git a/app/views/questions/edit.html.haml b/app/views/questions/edit.html.haml index 509ad418c..5c520769b 100644 --- a/app/views/questions/edit.html.haml +++ b/app/views/questions/edit.html.haml @@ -1 +1 @@ -= render partial: 'questions/form/steps', locals: { only_points: false } \ No newline at end of file += render partial: 'questions/form/steps' \ No newline at end of file From cdae5aceda38901213634fb8365631230fd61f2f Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Thu, 18 Dec 2025 08:16:19 -0300 Subject: [PATCH 44/70] fix: solve copy status error --- app/controllers/questions_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 8b51cd9b0..a6afcde46 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -130,6 +130,7 @@ def verify_edit_permissions render json: { action: 'edit', notify: edition_status == :edit_and_notify }, status: :ok else repository_action = edition_status == (:copy_blocked_exam || :copy_ownership) ? :copy : edition_status + repository_action = [:copy_blocked_exam, :copy_ownership].include?(edition_status) ? :copy : edition_status render json: { action: repository_action, alert: I18n.t(get_edit_permissions_alert_key(edition_status), scope: [:questions, :error]) }, status: :ok end From 158437bbfa262fe6fecc7f72d4b0f5f769325403 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Thu, 18 Dec 2025 10:05:11 -0300 Subject: [PATCH 45/70] fix: adjust only_points logic error --- app/controllers/exam_questions_controller.rb | 2 +- app/views/exam_questions/_question.html.haml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/exam_questions_controller.rb b/app/controllers/exam_questions_controller.rb index 293613716..2f75f72f7 100644 --- a/app/controllers/exam_questions_controller.rb +++ b/app/controllers/exam_questions_controller.rb @@ -69,7 +69,7 @@ def update authorize! :update, Question @exam_question = ExamQuestion.find params[:id] - if params['question_texts']['media_question'].to_i == 1 + if params['question_texts'].present? && params['question_texts']['media_question'].to_i == 1 if !params['question_texts_id'].blank? @question_text = QuestionText.find(params['question_texts_id']) diff --git a/app/views/exam_questions/_question.html.haml b/app/views/exam_questions/_question.html.haml index ab98db6a4..06429ea3b 100644 --- a/app/views/exam_questions/_question.html.haml +++ b/app/views/exam_questions/_question.html.haml @@ -54,6 +54,7 @@ .group_label= render 'questions/labels', labels: labels %td{ style: 'text-align:center;' }= question.type %td{ style: 'text-align:center;' }= (question.try(:score) rescue exam_question.score) + %td{ style: 'text-align:center;' }= exam_question.score %td{ style: 'text-align:center;' }= l(question.updated_at, format: :normal) %td{ style: 'text-align:center;', class: hide_columns ? 'invisible' : '' }= link_to content_tag(:i, nil, class: 'icon-eye', :'data-tooltip' => t('questions.question.preview')), "#void", onclick: 'preview_question(this)', :'data-url' => question_path(question), class: 'preview_question', disabled: !(can_see_preview && question.can_see?(true)) %td{ style: 'text-align:center;' }= question.privacy? ? content_tag(:i, nil, class: 'icon-lock', :'data-tooltip' => t('questions.question.private')) : content_tag(:i, nil, class: 'icon-minus', :"data-tooltip" => t('questions.question.public')) From db216511c869fae09ba1a3ae5bfe735dd0ed766b Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Thu, 18 Dec 2025 12:33:10 -0300 Subject: [PATCH 46/70] fix: adjust score render error --- app/views/exam_questions/_question.html.haml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/views/exam_questions/_question.html.haml b/app/views/exam_questions/_question.html.haml index 06429ea3b..9f91ec67b 100644 --- a/app/views/exam_questions/_question.html.haml +++ b/app/views/exam_questions/_question.html.haml @@ -53,7 +53,6 @@ - unless labels.blank? .group_label= render 'questions/labels', labels: labels %td{ style: 'text-align:center;' }= question.type - %td{ style: 'text-align:center;' }= (question.try(:score) rescue exam_question.score) %td{ style: 'text-align:center;' }= exam_question.score %td{ style: 'text-align:center;' }= l(question.updated_at, format: :normal) %td{ style: 'text-align:center;', class: hide_columns ? 'invisible' : '' }= link_to content_tag(:i, nil, class: 'icon-eye', :'data-tooltip' => t('questions.question.preview')), "#void", onclick: 'preview_question(this)', :'data-url' => question_path(question), class: 'preview_question', disabled: !(can_see_preview && question.can_see?(true)) From 2c7b85b27c09da3beadc70183392887bd9670e1c Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Thu, 18 Dec 2025 13:22:57 -0300 Subject: [PATCH 47/70] refactor: adjust fork_copy logic --- app/assets/javascripts/questions.js.erb | 21 ++++++++++++++------- app/views/exam_questions/_steps.html.haml | 2 ++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index 9db8bf736..4be44179a 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -270,11 +270,9 @@ function edit_question(icon, replace_list){ } } - if (['fork_copy', 'copy_repository', 'copy'].includes(data.action)) { - if (data.action != 'fork_copy'){ - if (!confirm(data.alert)) - return this; - } + if (['copy_repository', 'copy'].includes(data.action)) { + if (!confirm(data.alert)) + return this; var url_copy; @@ -293,15 +291,24 @@ function edit_question(icon, replace_list){ }); } } else { - var url_edit = $(icon).data('link-to-edit').replace(':id', ids) + (data.action === 'only_points' ? "?only_points=true" : ""); + var query_string = ''; if (data.notify) { if (!confirm("<%=I18n.t('questions.questions.notify_editors_alert')%>")) { return this; } - url_edit += "?notify_responsibles=true"; + query_string = "?notify_responsibles=true"; + } + + if (data.action === 'fork_copy') { + query_string = "?fork_copy=true"; + } else if (data.action === 'only_points') { + query_string = "?only_points=true"; } + var url_edit = $(icon).data('link-to-edit').replace(':id', ids) + query_string; + + $(icon).call_fancybox({ href : url_edit, open: true, diff --git a/app/views/exam_questions/_steps.html.haml b/app/views/exam_questions/_steps.html.haml index d66c01aa1..487c89889 100644 --- a/app/views/exam_questions/_steps.html.haml +++ b/app/views/exam_questions/_steps.html.haml @@ -1,5 +1,7 @@ .new_question = simple_form_for(@exam_question, html: { multipart: true, id: 'question_form' }) do |eq| + = hidden_field_tag :notify_responsibles, params[:notify_responsibles] + = hidden_field_tag :fork_copy, params[:fork_copy] %span.form_requirement= t(:required_fields) %h1#lightBoxDialogTitle= (eq.object.persisted? ? t('questions.form.steps.edit') : t('questions.form.steps.new')) = eq.input :exam_id, as: :hidden, input_html: { value: @exam_question.exam_id } From 9c761faa9380474f24a827c7435688594b61fa75 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Thu, 18 Dec 2025 13:23:56 -0300 Subject: [PATCH 48/70] feat: add email notification in exam_question controller --- app/controllers/exam_questions_controller.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/controllers/exam_questions_controller.rb b/app/controllers/exam_questions_controller.rb index 2f75f72f7..ee317542b 100644 --- a/app/controllers/exam_questions_controller.rb +++ b/app/controllers/exam_questions_controller.rb @@ -90,6 +90,13 @@ def update end if @exam_question.update_attributes exam_question_params + if params[:notify_responsibles] == 'true' + message = t('questions.questions.notification', question_enunciation: @exam_question.question.enunciation) + @exam_question.question.exams.each do |exam| + exam.notify_responsibles(message) + end + end + if !params['question_texts_id'].blank? && params['question_texts']['media_question'].to_i == 0 if Question.where(:question_text_id => params['question_texts_id']).count == 0 QuestionText.find(params['question_texts_id']).destroy From 937dd523bfd0d3916209844c7f0514a22ec6da25 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Mon, 22 Dec 2025 15:46:03 -0300 Subject: [PATCH 49/70] refactor: adjust fork_copy logic --- app/assets/javascripts/questions.js.erb | 52 +++++++++++--------- app/controllers/exam_questions_controller.rb | 16 +++++- app/controllers/questions_controller.rb | 1 - app/models/exam_question.rb | 12 ++--- app/views/exam_questions/_steps.html.haml | 11 +++-- app/views/exam_questions/edit.html.haml | 2 +- 6 files changed, 55 insertions(+), 39 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index 4be44179a..5d32234e2 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -262,17 +262,23 @@ function edit_question(icon, replace_list){ $.get(exam_id ? (base_verify_url + `/${exam_id}`) : base_verify_url, function(data){ - if (data.action === 'can_fork_copy') { - if (confirm(data.alert)) { - data.action = 'only_points'; - } else { - data.action = 'fork_copy'; + if (data.action === 'only_points') { + if (!confirm(data.alert)) + return this; + } else if (data.action === 'can_fork_copy') { + if (confirm(data.alert)) { + data.action = 'only_points'; + } else { + data.action = 'fork_copy'; + } } - } - if (['copy_repository', 'copy'].includes(data.action)) { - if (!confirm(data.alert)) - return this; + if (['copy_repository', 'copy', 'fork_copy'].includes(data.action)) { + + if (data.action !== 'fork_copy'){ + if (!confirm(data.alert)) + return this; + } var url_copy; @@ -282,8 +288,18 @@ function edit_question(icon, replace_list){ url_copy = $(icon).prev().data('link-to-copy').replace(':id', ids) + (data.action === 'fork_copy' ? "?fork_copy=true" : ""); } - $(icon).prev().call_fancybox({ href : url_copy, open: true, maxHeight: '70%', 'autoDimensions': false, 'autoSize': false, width: '700', height: 'auto', }); - if(replace_list != ''){ + $(icon).prev().call_fancybox({ + href : url_copy, + open: true, + maxHeight: '70%', + 'autoDimensions': false, + 'autoSize': false, + width: '700', + height: 'auto', + beforeClose: function() { clear_ckeditor(); } + }); + + if((replace_list !== '') && (data.action !== 'fork_copy')){ $.get($(replace_list).data("link-list"), function(data2){ $(replace_list).replaceWith(data2); flash_message(data.notice, 'notice'); @@ -291,24 +307,16 @@ function edit_question(icon, replace_list){ }); } } else { - var query_string = ''; + + var url_edit = $(icon).data('link-to-edit').replace(':id', ids) + (data.action === 'only_points' ? "?only_points=true" : ""); if (data.notify) { if (!confirm("<%=I18n.t('questions.questions.notify_editors_alert')%>")) { return this; } - query_string = "?notify_responsibles=true"; + url_edit += "?notify_responsibles=true"; } - if (data.action === 'fork_copy') { - query_string = "?fork_copy=true"; - } else if (data.action === 'only_points') { - query_string = "?only_points=true"; - } - - var url_edit = $(icon).data('link-to-edit').replace(':id', ids) + query_string; - - $(icon).call_fancybox({ href : url_edit, open: true, diff --git a/app/controllers/exam_questions_controller.rb b/app/controllers/exam_questions_controller.rb index ee317542b..58796f405 100644 --- a/app/controllers/exam_questions_controller.rb +++ b/app/controllers/exam_questions_controller.rb @@ -97,6 +97,15 @@ def update end end + if params[:forked_question_id].present? + forked_question = ExamQuestion.find(params[:forked_question_id]) + Rails.logger.info "\n\n\n exam_question.id #{@exam_question.question.id} \n\n\n" + Rails.logger.info "\n\n\n forked_question.id #{forked_question.question.id} \n\n\n" + forked_question.destroy + + log(forked_question.exam, "question: #{forked_question.question.id} [moved/deleted after copy] exam: #{forked_question.exam.id}", LogAction::TYPE[:update]) rescue nil + end + if !params['question_texts_id'].blank? && params['question_texts']['media_question'].to_i == 0 if Question.where(:question_text_id => params['question_texts_id']).count == 0 QuestionText.find(params['question_texts_id']).destroy @@ -373,8 +382,13 @@ def copy exam_question.can_save? exam_question.question.can_copy? @exam_question = ExamQuestion.copy(exam_question, current_user.id, is_fork_copy) + @forked_question = exam_question + + Rails.logger.info "\n\n\n exam_question.id #{@exam_question.question.id} \n\n\n" + Rails.logger.info "\n\n\n forked_question.id #{@forked_question.question.id} \n\n\n" + - log(ExamQuestion.exam, "question: #{exam_question.question_id} #{is_fork_copy ? '[fork_copy]' : '[copy]' }, #{exam_question.log_description}", LogAction::TYPE[:create]) rescue nil + log(ExamQuestion.exam, "question: #{exam_question.question_id} #{ '[copy]' }, #{exam_question.log_description}", LogAction::TYPE[:create]) rescue nil build_exam_question diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index a6afcde46..dd82f9838 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -129,7 +129,6 @@ def verify_edit_permissions authorize! :update, Question render json: { action: 'edit', notify: edition_status == :edit_and_notify }, status: :ok else - repository_action = edition_status == (:copy_blocked_exam || :copy_ownership) ? :copy : edition_status repository_action = [:copy_blocked_exam, :copy_ownership].include?(edition_status) ? :copy : edition_status render json: { action: repository_action, alert: I18n.t(get_edit_permissions_alert_key(edition_status), scope: [:questions, :error]) }, status: :ok end diff --git a/app/models/exam_question.rb b/app/models/exam_question.rb index 84c6a615f..37ae3b2bb 100644 --- a/app/models/exam_question.rb +++ b/app/models/exam_question.rb @@ -24,16 +24,10 @@ def recalculate_grades exam.recalculate_grades(nil,nil,nil,manually=true) end - def self.copy(exam_question_to_copy, user_id = nil, is_fork_copy = false) + def self.copy(exam_question_to_copy, user_id = nil, fork_copy = false) question = Question.copy(exam_question_to_copy.question, user_id) exam_question = ExamQuestion.create exam_question_to_copy.attributes.except('id', 'question_id', 'annulled').merge({ question_id: question.id, annulled: false }) - - if is_fork_copy - exam_question_to_copy.destroy - else - exam_question_to_copy.update_attributes annulled: true - end - + exam_question_to_copy.update_attributes annulled: true unless fork_copy exam_question end @@ -125,7 +119,7 @@ def log_description end def can_save? - raise 'cant_change_after_published' if exam.status && (new_record? || question.status) + raise 'cant_change_after_published' if exam.status && new_record? end def unpublish diff --git a/app/views/exam_questions/_steps.html.haml b/app/views/exam_questions/_steps.html.haml index 487c89889..baeb1c55f 100644 --- a/app/views/exam_questions/_steps.html.haml +++ b/app/views/exam_questions/_steps.html.haml @@ -1,7 +1,8 @@ .new_question = simple_form_for(@exam_question, html: { multipart: true, id: 'question_form' }) do |eq| = hidden_field_tag :notify_responsibles, params[:notify_responsibles] - = hidden_field_tag :fork_copy, params[:fork_copy] + - if @forked_question.present? + = hidden_field_tag :forked_question_id, @forked_question.id %span.form_requirement= t(:required_fields) %h1#lightBoxDialogTitle= (eq.object.persisted? ? t('questions.form.steps.edit') : t('questions.form.steps.new')) = eq.input :exam_id, as: :hidden, input_html: { value: @exam_question.exam_id } @@ -10,12 +11,12 @@ #steps %ul %li.info.active - - if only_points + - if @only_points.present? = 'Pontuação' -else = t('questions.form.steps.info') .dot.active#dot-info - - unless only_points + - unless @only_points.present? %li.medias = t('questions.form.steps.medias') .dot#dot-medias @@ -24,8 +25,8 @@ .dot#dot-items = eq.simple_fields_for :question do |f| .step-info - = render partial: 'questions/form/info', locals: { f: f, eq: eq, only_points: only_points } - - unless only_points + = render partial: 'questions/form/info', locals: { f: f, eq: eq, only_points: @only_points } + - unless @only_points.present? .step-medias.invisible = render partial: 'questions/form/media', locals: { f: f, eq: nil } .step-items.invisible diff --git a/app/views/exam_questions/edit.html.haml b/app/views/exam_questions/edit.html.haml index 0272c5203..d7742ca55 100644 --- a/app/views/exam_questions/edit.html.haml +++ b/app/views/exam_questions/edit.html.haml @@ -1,3 +1,3 @@ = javascript_include_tag 'questions' -= render partial: 'exam_questions/steps', locals: { only_points: @only_points } += render partial: 'exam_questions/steps' From d114b93a5f751552b108c0021e513f3b34a1f3ad Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 7 Jan 2026 12:35:27 -0300 Subject: [PATCH 50/70] refactor: adjust _item media forms --- app/assets/javascripts/questions.js.erb | 18 +++ .../stylesheets/partials/_questions.scss | 138 ++++++++---------- app/views/questions/form/_item.html.haml | 113 ++++++++++---- app/views/questions/form/_media.html.haml | 18 --- 4 files changed, 168 insertions(+), 119 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index 5d32234e2..f25c2a60e 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -455,6 +455,24 @@ function expand_or_compress_icon(icon){ } } +function toggle_alt_info(link){ + var parent = $(link).parent(); + parent.siblings().focus(); + var information = parent.siblings('.alt_info_text') + + if(information.hasClass('invisible')){ + information.removeClass('invisible') + parent.find('i.icon-arrow-up-triangle').removeClass('invisible'); + parent.find('i.icon-arrow-down-triangle').addClass('invisible'); + }else{ + information.addClass('invisible') + parent.find('i.icon-arrow-up-triangle').addClass('invisible'); + parent.find('i.icon-arrow-down-triangle').removeClass('invisible'); + } + + $(link).focus(); +} + function annul(icon){ if ($(icon).prop('disabled')) return false; diff --git a/app/assets/stylesheets/partials/_questions.scss b/app/assets/stylesheets/partials/_questions.scss index eb45ddb9e..3d23b3433 100644 --- a/app/assets/stylesheets/partials/_questions.scss +++ b/app/assets/stylesheets/partials/_questions.scss @@ -171,6 +171,14 @@ padding-top: 5px; } + .info { + display: flex; + flex-direction: column; + vertical-align: top; + text-align: center; + margin-right: 15px; + } + #items { .obs { color: $color_main; @@ -188,7 +196,6 @@ align-items: center; } - // width: 343px; display: inline-block; padding-top: 10px; @@ -206,8 +213,6 @@ a.image_icon, a.audio_icon { display: inline; - - //float: right; padding-bottom: 10px; padding-left: 2px; } @@ -226,7 +231,6 @@ } .duplicatable_nested_form { - // width: 515px; border-style: solid; border-color: #e8e8e8; border-radius: 20px; @@ -242,10 +246,6 @@ margin-left: 8px; } - /* .duplicatable_nested_form{ - padding-bottom: 20px; - } -*/ .label, .edit { display: none; @@ -266,28 +266,33 @@ } textarea { - // width: 270px; height: 80px; } - /* .duplicatable_nested_form{ - width: 515px; + .comment_area label { + text-align: left; } -*/ + .image { - margin-top: 10px; - margin-bottom: 10px; + margin: 20px 0 10px; + display: flex; + flex-direction: column; + text-align: center; + width: 100%; + + #item-image-container { + display: flex; + flex-direction: row; + justify-content: center; + padding: 0 0 8px 18px; + } input[type='file'], .input.file { display: none; } - text-align: center; - width: 100%; - .upload-preview { - // width: 136px; display: inline-block; text-align: center; flex-direction: column; @@ -299,10 +304,7 @@ } .add_file { - // margin-left: -30px; - // display: inline; width: 136px; - display: block; } i.icon-trash { @@ -311,39 +313,55 @@ } input[type='text'] { - // width: 316px; margin-top: 5px; + width: 98%; + max-width: 100%; } .info { - // display: inline-block; - vertical-align: top; + margin-top: 10px; - input { + .input { + display: flex; + flex-direction: row; + justify-content: center; margin-top: 5px; - // width: 280px; + + label { + margin-right: 2px; + } } - textarea { - // width: 271px; - height: 90px; + .invisible { + display: none; + } + + input { + margin-top: 5px; } label.alt { - text-align: left; - // margin-top: -5px; - // margin-left: 80px; - width: 281px; + text-align: center; + width: 100%; } .input label { - width: 20px; + width: 65px; } } } .audio { - margin-top: 10px; + margin: 20px 0 10px; + display: flex; + flex-direction: column; + + #item-audio-container { + display: flex; + flex-direction: row; + justify-content: center; + padding: 0 0 8px 18px; + } input[type='file'], .input.file { @@ -351,24 +369,16 @@ } .upload-preview { - // width: 136px; - // display: inline-block; - // text-align: center; - display: flex; flex-direction: column; justify-content: center; align-items: center; audio { - // max-width: 160px; - // max-height: 30px; vertical-align: middle; } .add_file { - // margin-left: -30px; - // display: inline; display: block; width: 150px; margin-bottom: 10px; @@ -380,43 +390,28 @@ } } - input[type='text'] { - // width: 316px; - width: 100%; - margin-top: 5px; - } - .info { - // display: inline-block; - vertical-align: top; - - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - input { - margin-top: 5px; - width: 280px; - } + margin-top: 10px; - input { - width: 271px; + .invisible { + display: none; } - .input label { - display: inline; + .input { + display: flex; + flex-direction: row; + justify-content: center; + margin-top: 5px; } .input textarea { width: 100%; + max-width: 100%; + height: 70px; } label.alt { - // text-align: left; margin-top: -5px; - // margin-left: 80px; - // width: 281px; width: 100%; text-align: center; } @@ -428,8 +423,6 @@ width: 100%; textarea { - // width: 409px; - // height: 30px; width: 100%; } } @@ -480,7 +473,6 @@ .upload-preview { text-align: center; - display: flex; flex-direction: column; justify-content: center; @@ -554,16 +546,12 @@ } .upload-preview { - // width: 190px; - // display: inline-block; text-align: center; - // margin-left: 24px; display: flex; flex-direction: column; justify-content: center; align-items: center; - text-align: center; audio { max-width: 160px; diff --git a/app/views/questions/form/_item.html.haml b/app/views/questions/form/_item.html.haml index 2ff94bc06..4d5b6c191 100644 --- a/app/views/questions/form/_item.html.haml +++ b/app/views/questions/form/_item.html.haml @@ -17,47 +17,69 @@ = i.select :value, ([ ['V', true], ['F', false]]), include_blank: false .obs= t('questions.form.items.choose_correct') .label= i.label :description + .desc = i.input :description, label: false, class: 'ckeditor' + .comment_area{ class: i.object.comment.blank? ? 'hide' : 'comment_item' , :style=>"display:none;"} = i.input :comment, as: :text, label: t('questions.form.items.comment') + .image.hide{:style=>"display:none;"} .upload-preview - - if i.object.item_image_file_name.blank? - %img{ src: '' } - - else - %img{ src: i.object.item_image.as_json, id: 'image_item' } + #item-image-container + - if i.object.item_image_file_name.blank? + %img{ src: '' } + - else + %img{ src: i.object.item_image.as_json, id: 'image_item' } + + - if i.object.item_image_file_name + = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: "remove_file_item(this, 'img')", :'data-url' => remove_image_item_exam_question_path(i.object, question_id: i.object.question.id) + = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: 'remove_file_item_tmp(this, "img")', class: 'trash-tmp', style: 'display: none' + - else + = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: 'remove_file_item_tmp(this, "img")' = i.input :item_image, as: :file, label: false - - if i.object.item_image_file_name - = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: "remove_file_item(this, 'img')", :'data-url' => remove_image_item_exam_question_path(i.object, question_id: i.object.question.id) - = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: 'remove_file_item_tmp(this, "img")', class: 'trash-tmp', style: 'display: none' - - else - = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: 'remove_file_item_tmp(this, "img")' - = link_to t('questions.form.items.add_image'), "#void", onclick: 'add_file(this)', class: 'btn btn_main add_file' .info = i.input :img_alt, label: t('questions.form.items.alt') - = i.label t('questions.form.items.alt_desc'), class: 'alt' + + .alt_info.title + =link_to "#void", onclick: 'toggle_alt_info(this)', onkeydown: 'click_on_keypress(event, this);' do + =raw t('questions.form.image.alt_title') + %i.icon-arrow-down-triangle + %i.icon-arrow-up-triangle.invisible + .invisible.alt_info_text{tabindex: 0} + = i.label t('questions.form.image.alt_desc'), class: 'alt' + .audio.hide{:style=>"display:none;"} .upload-preview - - if i.object.item_audio_file_name.blank? - %audio{ src: '' } - - else - %audio{ src: i.object.item_audio.as_json, autoplay: false, controls: true, name: 'audioQuestion', id: 'audio_item'} - %p= t("errors.messages.audio") - = i.input :item_audio, as: :file, label: false - - if i.object.item_audio_file_name - = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: 'remove_file_item(this, "audio")', :'data-url' => remove_audio_item_exam_question_path(i.object, question_id: i.object.question.id) - = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: "remove_file_item_tmp(this, 'audio')", class: 'trash-tmp', style: 'display: none' - - else - = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: "remove_file_item_tmp(this, 'audio')" + #item-audio-container + - if i.object.item_audio_file_name.blank? + %audio{ src: '' } + - else + %audio{ src: i.object.item_audio.as_json, autoplay: false, controls: true, name: 'audioQuestion', id: 'audio_item'} + %p= t("errors.messages.audio") + + - if i.object.item_audio_file_name + = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: 'remove_file_item(this, "audio")', :'data-url' => remove_audio_item_exam_question_path(i.object, question_id: i.object.question.id) + = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: "remove_file_item_tmp(this, 'audio')", class: 'trash-tmp', style: 'display: none' + - else + = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: "remove_file_item_tmp(this, 'audio')" + = i.input :item_audio, as: :file, label: false = link_to t('questions.form.items.add_audio'), "#void", onclick: 'add_file(this)', class: 'btn btn_main add_file' + .info = i.input :audio_description, as: :text, label: t('questions.form.items.audio_description') - = i.label t('questions.form.audio.aud_desc'), class: 'alt' + + .alt_info.title + =link_to "#void", onclick: 'toggle_alt_info(this)', onkeydown: 'click_on_keypress(event, this);' do + =raw t('questions.form.audio.alt_title') + %i.icon-arrow-down-triangle + %i.icon-arrow-up-triangle.invisible + .invisible.alt_info_text{tabindex: 0} + = i.label t('questions.form.audio.aud_desc'), class: 'alt' -else - count += 1 @@ -92,6 +114,8 @@ var ckb = $(this).parent().find('input[type="checkbox"]'); ckb.prop('checked', !ckb.prop('checked')); }); + + hide_trash_if_empty(); }); $('#items .duplicatable_nested_form').each(function(idx) { @@ -108,8 +132,43 @@ }); expand_existing_options(); + + hide_trash_if_empty(); + + $(document).on('change', '.image input[type="file"], .audio input[type="file"]', function(){ + if($(this).val()){ + $(this).parents('.upload-preview:first').find('a:has(.icon-trash)').show(); + + var trash_tmp = $(this).parents('.upload-preview:first').find('.trash-tmp'); + + if(trash_tmp.length > 0 && trash_tmp.is(':hidden')){ + trash_tmp.show(); + } else { + $(this).parents('.upload-preview:first').find('a:has(.icon-trash)').not('.trash-tmp[style*="display: none"]').show(); + $(this).parents('.upload-preview:first').find('audio').show(); + $(this).parents('.upload-preview:first').find('img').show(); + } + } + }); }); + function hide_trash_if_empty(){ + $('.upload-preview').each(function(){ + var img = $(this).find('img'); + var audio = $(this).find('audio'); + var hasContent = false; + + + if(img.length > 0 && img.attr('src') != '' && img.attr('src') != null) hasContent = true; + + if(audio.length > 0 && audio.attr('src') != '' && audio.attr('src') != null) hasContent = true; + + if(!hasContent){ + $(this).find('a:has(.icon-trash)').hide(); + } + }); + } + function add_comment(btn){ $(btn).parents('.duplicatable_nested_form:first').find('.comment_area').fadeToggle(function(){ if ($(this).is(':visible')) @@ -145,8 +204,10 @@ function remove_file_item_tmp(link, type){ $(link).parents('.upload-preview:first').find(type).attr('src', ''); $(link).parents('.upload-preview:first').find('input.file').val(''); + $(link).parent().find(type).hide(); + $(link).hide(); - if(type == 'audio') + if(type === 'audio') $(link).parents('.upload-preview:first').parent().find('.info .text textarea').val(''); else $(link).parents('.upload-preview:first').parent().find('.info .string input').val(''); @@ -155,7 +216,7 @@ function add_image(btn){ $(btn).parents('.duplicatable_nested_form:first').find('.image').fadeToggle(function(){ if ($(this).is(':visible')) - $(this).css('display','inline-block'); + // $(this).css('display','inline-block'); $(this).toggleClass('hide'); }); } @@ -163,7 +224,7 @@ function add_audio(btn){ $(btn).parents('.duplicatable_nested_form:first').find('.audio').fadeToggle(function(){ if ($(this).is(':visible')) - $(this).css('display','inline-block'); + // $(this).css('display','inline-block'); $(this).toggleClass('hide'); }); } diff --git a/app/views/questions/form/_media.html.haml b/app/views/questions/form/_media.html.haml index 9b85cea2f..abc9f7790 100644 --- a/app/views/questions/form/_media.html.haml +++ b/app/views/questions/form/_media.html.haml @@ -61,21 +61,3 @@ }); }) - - function toggle_alt_info(link){ - var parent = $(link).parent(); - parent.siblings().focus(); - var information = parent.siblings('.alt_info_text') - - if(information.hasClass('invisible')){ - information.removeClass('invisible') - parent.find('i.icon-arrow-up-triangle').removeClass('invisible'); - parent.find('i.icon-arrow-down-triangle').addClass('invisible'); - }else{ - information.addClass('invisible') - parent.find('i.icon-arrow-up-triangle').addClass('invisible'); - parent.find('i.icon-arrow-down-triangle').removeClass('invisible'); - } - - $(link).focus(); - } \ No newline at end of file From 74c8815add8f05d0388978316502b67db9e3c0fb Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 7 Jan 2026 12:37:09 -0300 Subject: [PATCH 51/70] refactor: add arrow behavior to info page. --- app/views/questions/form/_info.html.haml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/views/questions/form/_info.html.haml b/app/views/questions/form/_info.html.haml index 518695ca9..1053134fd 100644 --- a/app/views/questions/form/_info.html.haml +++ b/app/views/questions/form/_info.html.haml @@ -20,6 +20,7 @@ =link_to('#void', onclick: 'toggle_text(this);' , onkeydown: 'click_on_keypress(event, this); ') do = t('questions.question.understanding_score_calculation') %i.icon-arrow-down-triangle{:'aria-hidden'=>'true'} + %i.icon-arrow-up-triangle.invisible{:'aria-hidden'=>'true'} .correction_text .invisible.true_or_false_calculation = raw(t('questions.types.true_or_false_calculation', score_item: t('questions.question.total_amount'))) @@ -212,11 +213,12 @@ } function toggle_text(link){ - type = $('#exam_question_question_attributes_type_question').length + var type = $('#exam_question_question_attributes_type_question').length ? $('#exam_question_question_attributes_type_question').val() : $('#question_type_question').val(); - div = $('.correction_text').children(':visible'); + var div = $('.correction_text').children(':visible'); + var selected_div; if(type==0){ selected_div = $('.unique_choice_calculation.invisible'); @@ -228,12 +230,16 @@ } } - selected_div.toggleClass('invisible'); - div.toggleClass('invisible'); - - if(!!div.is(':visible')){ + if(selected_div.hasClass('invisible')){ focus_element(selected_div); + $(link).parent().find('i.icon-arrow-up-triangle').removeClass('invisible'); + $(link).parent().find('i.icon-arrow-down-triangle').addClass('invisible'); }else{ focus_element($(link).parent()); + $(link).parent().find('i.icon-arrow-up-triangle').addClass('invisible'); + $(link).parent().find('i.icon-arrow-down-triangle').removeClass('invisible'); } + + selected_div.toggleClass('invisible'); + div.toggleClass('invisible'); } From b9e9090d64b5d9526093e07fcd5936b982334e9b Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 7 Jan 2026 12:37:45 -0300 Subject: [PATCH 52/70] refactor: minor image and audio forms adjusts --- .../stylesheets/partials/_questions.scss | 51 ++++++++++--------- .../stylesheets/partials/_responsive.scss | 3 +- app/views/questions/form/_audio.html.haml | 5 +- app/views/questions/form/_image.html.haml | 5 +- 4 files changed, 36 insertions(+), 28 deletions(-) diff --git a/app/assets/stylesheets/partials/_questions.scss b/app/assets/stylesheets/partials/_questions.scss index 3d23b3433..47b9b3dd5 100644 --- a/app/assets/stylesheets/partials/_questions.scss +++ b/app/assets/stylesheets/partials/_questions.scss @@ -125,7 +125,8 @@ margin-left: -3px; margin-bottom: 10px; - .correction_info div, .correction_info a{ + .correction_info div, + .correction_info a{ padding-left: 22px; padding-top: 5px; vertical-align: top; @@ -171,6 +172,10 @@ padding-top: 5px; } + #media-inputs-container { + margin-right: 4%; + } + .info { display: flex; flex-direction: column; @@ -435,7 +440,7 @@ border-style: solid; border-color: #e8e8e8; border-radius: 20px; - padding: 10px 0px 20px 0px; + padding: 10px 0 20px 0; margin-bottom: 10px; text-align: center; @@ -451,15 +456,19 @@ input[type='text'] { margin-top: 5px; - width: 50%; + width: 100%; + max-width: 100%; } .info { - display: flex; - flex-direction: column; - vertical-align: top; - text-align: center; - margin: 0 15px; + + .input { + display: flex; + flex-direction: row; + justify-content: center; + margin-top: 5px; + max-width: 100%; + } label.alt { display: inline; @@ -484,8 +493,7 @@ } .add_file { - margin: 4px; - margin-bottom: 20px; + margin: 4px 4px 24px; } } } @@ -498,7 +506,7 @@ border-style: solid; border-color: #e8e8e8; border-radius: 20px; - padding: 10px 0px 20px 0px; + padding: 10px 0 20px 0; margin-bottom: 10px; text-align: center; @@ -513,23 +521,21 @@ } textarea { - width: 50%; + width: 100%; height: 70px; } input[type='text'] { margin-top: 5px; - width: 50%; + width: 100%; + max-width: 100%; } .info { - display: flex; - flex-direction: column; - vertical-align: top; - text-align: center; - margin: 0 15px; - - input { + .input { + display: flex; + flex-direction: row; + justify-content: center; margin-top: 5px; } @@ -559,14 +565,13 @@ } .add_file { - margin: 4px; - margin-bottom: 20px; + margin: 4px 4px 24px; } } } .alt_info { - margin-top: 10px; + margin-top: 5px; label { margin-left: 5px; diff --git a/app/assets/stylesheets/partials/_responsive.scss b/app/assets/stylesheets/partials/_responsive.scss index 0a181fe38..45168b9d3 100644 --- a/app/assets/stylesheets/partials/_responsive.scss +++ b/app/assets/stylesheets/partials/_responsive.scss @@ -1043,7 +1043,8 @@ } } - #audios .upload-preview { + #audios .upload-preview, + #items .audio .upload-preview { audio { max-width: 160px; } diff --git a/app/views/questions/form/_audio.html.haml b/app/views/questions/form/_audio.html.haml index 26f09c2aa..df729dcd2 100644 --- a/app/views/questions/form/_audio.html.haml +++ b/app/views/questions/form/_audio.html.haml @@ -17,8 +17,9 @@ = link_to t('.add_audio'), "#void", onclick: 'add_file(this)', class: 'btn btn_main add_file' .info - = aud.input :audio_description, as: :text - = aud.input :description + #media-inputs-container + = aud.input :audio_description, as: :text + = aud.input :description .alt_info.title = link_to "#void", onclick: 'toggle_alt_info(this)', onkeydown: 'click_on_keypress(event, this);' do diff --git a/app/views/questions/form/_image.html.haml b/app/views/questions/form/_image.html.haml index 027af754a..5b0b9e81e 100644 --- a/app/views/questions/form/_image.html.haml +++ b/app/views/questions/form/_image.html.haml @@ -16,8 +16,9 @@ = link_to t('.add_image'), "#void", onclick: 'add_file(this)', class: 'btn btn_main add_file' .info - = img.input :legend - = img.input :img_alt + #media-inputs-container + = img.input :legend + = img.input :img_alt .alt_info.title = link_to "#void", onclick: 'toggle_alt_info(this)', onkeydown: 'click_on_keypress(event, this);' do From 700b51c5c117193ab6efb30159533639def63df1 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 7 Jan 2026 15:56:19 -0300 Subject: [PATCH 53/70] chore: remove new question form layout editions --- app/assets/javascripts/questions.js.erb | 18 -- .../stylesheets/partials/_questions.scss | 265 +++++++++--------- .../stylesheets/partials/_responsive.scss | 10 +- app/views/questions/form/_audio.html.haml | 16 +- app/views/questions/form/_image.html.haml | 16 +- app/views/questions/form/_info.html.haml | 7 +- app/views/questions/form/_item.html.haml | 119 ++------ app/views/questions/form/_label.html.haml | 4 +- app/views/questions/form/_media.html.haml | 6 +- config/locales/en_US.yml | 6 +- config/locales/pt_BR.yml | 6 +- 11 files changed, 180 insertions(+), 293 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index f25c2a60e..5d32234e2 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -455,24 +455,6 @@ function expand_or_compress_icon(icon){ } } -function toggle_alt_info(link){ - var parent = $(link).parent(); - parent.siblings().focus(); - var information = parent.siblings('.alt_info_text') - - if(information.hasClass('invisible')){ - information.removeClass('invisible') - parent.find('i.icon-arrow-up-triangle').removeClass('invisible'); - parent.find('i.icon-arrow-down-triangle').addClass('invisible'); - }else{ - information.addClass('invisible') - parent.find('i.icon-arrow-up-triangle').addClass('invisible'); - parent.find('i.icon-arrow-down-triangle').removeClass('invisible'); - } - - $(link).focus(); -} - function annul(icon){ if ($(icon).prop('disabled')) return false; diff --git a/app/assets/stylesheets/partials/_questions.scss b/app/assets/stylesheets/partials/_questions.scss index 47b9b3dd5..42848a9a9 100644 --- a/app/assets/stylesheets/partials/_questions.scss +++ b/app/assets/stylesheets/partials/_questions.scss @@ -1,6 +1,5 @@ .questions, -[class^='list_questions_exam_'], -.exam_questions { +[class^='list_questions_exam_'] { .search { label { word-wrap: break-word; @@ -125,8 +124,7 @@ margin-left: -3px; margin-bottom: 10px; - .correction_info div, - .correction_info a{ + .correction_info div, .correction_info a{ padding-left: 22px; padding-top: 5px; vertical-align: top; @@ -172,18 +170,6 @@ padding-top: 5px; } - #media-inputs-container { - margin-right: 4%; - } - - .info { - display: flex; - flex-direction: column; - vertical-align: top; - text-align: center; - margin-right: 15px; - } - #items { .obs { color: $color_main; @@ -201,6 +187,7 @@ align-items: center; } + // width: 343px; display: inline-block; padding-top: 10px; @@ -218,6 +205,8 @@ a.image_icon, a.audio_icon { display: inline; + + //float: right; padding-bottom: 10px; padding-left: 2px; } @@ -236,6 +225,7 @@ } .duplicatable_nested_form { + // width: 515px; border-style: solid; border-color: #e8e8e8; border-radius: 20px; @@ -251,6 +241,10 @@ margin-left: 8px; } + /* .duplicatable_nested_form{ + padding-bottom: 20px; + } +*/ .label, .edit { display: none; @@ -271,33 +265,28 @@ } textarea { + // width: 270px; height: 80px; } - .comment_area label { - text-align: left; + /* .duplicatable_nested_form{ + width: 515px; } - +*/ .image { - margin: 20px 0 10px; - display: flex; - flex-direction: column; - text-align: center; - width: 100%; - - #item-image-container { - display: flex; - flex-direction: row; - justify-content: center; - padding: 0 0 8px 18px; - } + margin-top: 10px; + margin-bottom: 10px; input[type='file'], .input.file { display: none; } + text-align: center; + width: 100%; + .upload-preview { + // width: 136px; display: inline-block; text-align: center; flex-direction: column; @@ -309,7 +298,10 @@ } .add_file { + // margin-left: -30px; + // display: inline; width: 136px; + display: block; } i.icon-trash { @@ -318,55 +310,39 @@ } input[type='text'] { + // width: 316px; margin-top: 5px; - width: 98%; - max-width: 100%; } .info { - margin-top: 10px; + // display: inline-block; + vertical-align: top; - .input { - display: flex; - flex-direction: row; - justify-content: center; + input { margin-top: 5px; - - label { - margin-right: 2px; - } + // width: 280px; } - .invisible { - display: none; - } - - input { - margin-top: 5px; + textarea { + // width: 271px; + height: 90px; } label.alt { - text-align: center; - width: 100%; + text-align: left; + // margin-top: -5px; + // margin-left: 80px; + width: 281px; } .input label { - width: 65px; + width: 20px; } } } .audio { - margin: 20px 0 10px; - display: flex; - flex-direction: column; - - #item-audio-container { - display: flex; - flex-direction: row; - justify-content: center; - padding: 0 0 8px 18px; - } + margin-top: 10px; input[type='file'], .input.file { @@ -374,16 +350,24 @@ } .upload-preview { + // width: 136px; + // display: inline-block; + // text-align: center; + display: flex; flex-direction: column; justify-content: center; align-items: center; audio { + // max-width: 160px; + // max-height: 30px; vertical-align: middle; } .add_file { + // margin-left: -30px; + // display: inline; display: block; width: 150px; margin-bottom: 10px; @@ -395,28 +379,43 @@ } } + input[type='text'] { + // width: 316px; + width: 100%; + margin-top: 5px; + } + .info { - margin-top: 10px; + // display: inline-block; + vertical-align: top; - .invisible { - display: none; - } + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; - .input { - display: flex; - flex-direction: row; - justify-content: center; + input { margin-top: 5px; + width: 280px; + } + + input { + width: 271px; + } + + .input label { + display: inline; } .input textarea { width: 100%; - max-width: 100%; - height: 70px; } label.alt { + // text-align: left; margin-top: -5px; + // margin-left: 80px; + // width: 281px; width: 100%; text-align: center; } @@ -428,6 +427,8 @@ width: 100%; textarea { + // width: 409px; + // height: 30px; width: 100%; } } @@ -435,42 +436,50 @@ #images { .duplicatable_nested_form { - display: flex; - flex-direction: column; border-style: solid; border-color: #e8e8e8; border-radius: 20px; - padding: 10px 0 20px 0; + padding: 10px 0px 20px 0px; margin-bottom: 10px; text-align: center; .remove_nested_fields_link { - text-align: right; - margin: 5px 15px; + margin-right: 10px; + float: right; } .input.text.optional { margin-top: 10px; + + // textarea { + // width: 300px; + // } } } + textarea { + height: 60px; + } + input[type='text'] { + // width: 294px; margin-top: 5px; - width: 100%; - max-width: 100%; } .info { + // text-align: right; + display: inline-block; + vertical-align: top; + text-align: center; - .input { - display: flex; - flex-direction: row; - justify-content: center; + input { margin-top: 5px; - max-width: 100%; } label.alt { + // width: 300px; + // text-align: left; + // margin-top: -5px; display: inline; } } @@ -481,7 +490,11 @@ } .upload-preview { + // width: 190px; + // display: inline-block; text-align: center; + // margin-left: 24px; + display: flex; flex-direction: column; justify-content: center; @@ -493,26 +506,24 @@ } .add_file { - margin: 4px 4px 24px; + margin: 4px; + margin-bottom: 20px; } } } #audios { .duplicatable_nested_form { - display: flex; - flex-direction: column; - border-style: solid; border-color: #e8e8e8; border-radius: 20px; - padding: 10px 0 20px 0; + padding: 10px 0px 20px 0px; margin-bottom: 10px; text-align: center; .remove_nested_fields_link { - text-align: right; - margin: 5px 15px; + float: right; + margin-right: 10px; } .input.text.optional { @@ -521,28 +532,31 @@ } textarea { - width: 100%; - height: 70px; + // width: 300px; + width: 85%; + height: 30px; } input[type='text'] { + // width: 294px; margin-top: 5px; - width: 100%; - max-width: 100%; } .info { - .input { - display: flex; - flex-direction: row; - justify-content: center; + // text-align: right; + display: inline-block; + vertical-align: top; + + input { margin-top: 5px; } label.alt, .exam_question_question_question_audios_audio_description label { + // width: 300px; + // text-align: left; + // margin-top: -5px; display: inline; - margin: 0 10px; } } @@ -552,12 +566,16 @@ } .upload-preview { + // width: 190px; + // display: inline-block; text-align: center; + // margin-left: 24px; display: flex; flex-direction: column; justify-content: center; align-items: center; + text-align: center; audio { max-width: 160px; @@ -565,62 +583,39 @@ } .add_file { - margin: 4px 4px 24px; + margin: 4px; + margin-bottom: 20px; } } } - .alt_info { - margin-top: 5px; - - label { - margin-left: 5px; - } - } - .labels { - display: flex; - flex-direction: row; - - #labels_container { - flex-direction: column; - } - - label { - flex-shrink: 0; - padding-right: 10px; - } - input { width: 100px; } .nested_question_question_labels, .nested_exam_question_question_question_labels { + display: inline-block; margin-right: 10px; - .duplicatable_nested_form { - display: flex; - flex-direction: row; - justify-content: end; - margin-top: 10px; - - a.remove_nested_fields_link { - margin-left: 2px; - margin-top: 2px; - } + a.remove_nested_fields_link { + float: right; } } #labels { - display: flex; - flex-direction: row; - flex-wrap: wrap; + .nested_exam_question_question_question_labels { + .input.string { + display: inline; + } + } } } .add_nested_fields_link { - margin: 10px 0; + margin: 10px; + margin-left: 35px } } @@ -822,7 +817,7 @@ } } -.text_labels { +.labels { #question_text { //width: 500px; diff --git a/app/assets/stylesheets/partials/_responsive.scss b/app/assets/stylesheets/partials/_responsive.scss index 45168b9d3..2ce4c96ed 100644 --- a/app/assets/stylesheets/partials/_responsive.scss +++ b/app/assets/stylesheets/partials/_responsive.scss @@ -731,13 +731,6 @@ text-align: left; } - .question_question_images_legend .form_label, - .question_question_images_img_alt .form_label, - .question_question_audios_description .form_label, - label.text.optional.control-label.form_label { - display: inline-block; - } - label { display: block; } @@ -1043,8 +1036,7 @@ } } - #audios .upload-preview, - #items .audio .upload-preview { + #audios .upload-preview { audio { max-width: 160px; } diff --git a/app/views/questions/form/_audio.html.haml b/app/views/questions/form/_audio.html.haml index df729dcd2..e99abee3d 100644 --- a/app/views/questions/form/_audio.html.haml +++ b/app/views/questions/form/_audio.html.haml @@ -5,7 +5,6 @@ - if (f.object.new_record? || aud.object.persisted? || @count == count_audios + 1) - @count += 1 .duplicatable_nested_form - = aud.remove_nested_fields_link content_tag(:i, nil, class: 'icon-trash') .upload-preview - if aud.object.audio.blank? %audio{ src: '' } @@ -17,17 +16,10 @@ = link_to t('.add_audio'), "#void", onclick: 'add_file(this)', class: 'btn btn_main add_file' .info - #media-inputs-container - = aud.input :audio_description, as: :text - = aud.input :description - - .alt_info.title - = link_to "#void", onclick: 'toggle_alt_info(this)', onkeydown: 'click_on_keypress(event, this);' do - =raw t('.alt_title') - %i.icon-arrow-down-triangle - %i.icon-arrow-up-triangle.invisible - .invisible.alt_info_text{tabindex: 0} - = aud.label t('.aud_desc'), class: 'alt' + = aud.remove_nested_fields_link content_tag(:i, nil, class: 'icon-trash') + = aud.input :audio_description, as: :text + = aud.input :description + = aud.label t('.aud_desc'), class: 'alt' -else - @count += 1 diff --git a/app/views/questions/form/_image.html.haml b/app/views/questions/form/_image.html.haml index 5b0b9e81e..e7d6a81e4 100644 --- a/app/views/questions/form/_image.html.haml +++ b/app/views/questions/form/_image.html.haml @@ -5,7 +5,6 @@ - if (f.object.new_record? || img.object.persisted? || @count == count_images + 1) - @count += 1 .duplicatable_nested_form - = img.remove_nested_fields_link content_tag(:i, nil, class: 'icon-trash') .upload-preview - if img.object.image.blank? %img{ src: '' } @@ -16,17 +15,10 @@ = link_to t('.add_image'), "#void", onclick: 'add_file(this)', class: 'btn btn_main add_file' .info - #media-inputs-container - = img.input :legend - = img.input :img_alt - - .alt_info.title - = link_to "#void", onclick: 'toggle_alt_info(this)', onkeydown: 'click_on_keypress(event, this);' do - =raw t('.alt_title') - %i.icon-arrow-down-triangle - %i.icon-arrow-up-triangle.invisible - .invisible.alt_info_text{tabindex: 0} - = img.label t('.alt_desc'), class: 'alt' + = img.remove_nested_fields_link content_tag(:i, nil, class: 'icon-trash') + = img.input :legend + = img.input :img_alt + = img.label t('.alt_desc'), class: 'alt' -else - @count += 1 diff --git a/app/views/questions/form/_info.html.haml b/app/views/questions/form/_info.html.haml index 1053134fd..dbb34fa52 100644 --- a/app/views/questions/form/_info.html.haml +++ b/app/views/questions/form/_info.html.haml @@ -32,10 +32,9 @@ - unless only_points .labels = f.label t('questions.questions.labels'), class: 'label_labels' - %div#labels_container - = render partial: 'questions/form/label', locals: { f: f } - .input.string - = f.add_nested_fields_link :question_labels, t('.add_labels'), class: 'btn' + = render partial: 'questions/form/label', locals: { f: f } + .input.string + = f.add_nested_fields_link :question_labels, t('.add_labels'), class: 'btn' = f.input :enunciation, as: :ckeditor diff --git a/app/views/questions/form/_item.html.haml b/app/views/questions/form/_item.html.haml index 4d5b6c191..813da4187 100644 --- a/app/views/questions/form/_item.html.haml +++ b/app/views/questions/form/_item.html.haml @@ -8,78 +8,56 @@ %legend#question_images_label = f.label t('questions.form.items.item'), id: 'fs_legend_item' = i.remove_nested_fields_link content_tag(:i, nil, class: 'icon-trash', :'data-tooltip' => t('questions.form.items.remove')) - = link_to content_tag(:i, nil, class: 'icon-comment', :'data-tooltip' => t('questions.form.items.comments')), "#void", onclick: 'add_comment(this)', class: 'comment' - = link_to content_tag(:i, nil, class: 'icon-pictures', :'data-tooltip' => t('questions.form.items.images')), "#void", onclick: 'add_image(this)', class: 'image_icon' - = link_to content_tag(:i, nil, class: 'icon-music', :'data-tooltip' => t('questions.form.items.audios')), "#void", onclick: 'add_audio(this)', class: 'audio_icon' + = link_to content_tag(:i, nil, class: 'icon-comment'), "#void", onclick: 'add_comment(this)', class: 'comment', :'data-tooltip' => t('questions.form.items.comments') + = link_to content_tag(:i, nil, class: 'icon-pictures'), "#void", onclick: 'add_image(this)', class: 'image_icon', :'data-tooltip' => t('questions.form.items.images') + = link_to content_tag(:i, nil, class: 'icon-music'), "#void", onclick: 'add_audio(this)', class: 'audio_icon', :'data-tooltip' => t('questions.form.items.audios') .inline .inputs = i.check_box :value = i.select :value, ([ ['V', true], ['F', false]]), include_blank: false .obs= t('questions.form.items.choose_correct') .label= i.label :description - .desc = i.input :description, label: false, class: 'ckeditor' - .comment_area{ class: i.object.comment.blank? ? 'hide' : 'comment_item' , :style=>"display:none;"} = i.input :comment, as: :text, label: t('questions.form.items.comment') - .image.hide{:style=>"display:none;"} .upload-preview - #item-image-container - - if i.object.item_image_file_name.blank? - %img{ src: '' } - - else - %img{ src: i.object.item_image.as_json, id: 'image_item' } - - - if i.object.item_image_file_name - = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: "remove_file_item(this, 'img')", :'data-url' => remove_image_item_exam_question_path(i.object, question_id: i.object.question.id) - = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: 'remove_file_item_tmp(this, "img")', class: 'trash-tmp', style: 'display: none' - - else - = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: 'remove_file_item_tmp(this, "img")' + - if i.object.item_image_file_name.blank? + %img{ src: '' } + - else + %img{ src: i.object.item_image.as_json, id: 'image_item' } = i.input :item_image, as: :file, label: false + - if i.object.item_image_file_name + = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: "remove_file_item(this, 'img')", :'data-url' => remove_image_item_exam_question_path(i.object, question_id: i.object.question.id) + = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: 'remove_file_item_tmp(this, "img")', class: 'trash-tmp', style: 'display: none' + - else + = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: 'remove_file_item_tmp(this, "img")' + = link_to t('questions.form.items.add_image'), "#void", onclick: 'add_file(this)', class: 'btn btn_main add_file' .info = i.input :img_alt, label: t('questions.form.items.alt') - - .alt_info.title - =link_to "#void", onclick: 'toggle_alt_info(this)', onkeydown: 'click_on_keypress(event, this);' do - =raw t('questions.form.image.alt_title') - %i.icon-arrow-down-triangle - %i.icon-arrow-up-triangle.invisible - .invisible.alt_info_text{tabindex: 0} - = i.label t('questions.form.image.alt_desc'), class: 'alt' - + = i.label t('questions.form.items.alt_desc'), class: 'alt' .audio.hide{:style=>"display:none;"} .upload-preview - #item-audio-container - - if i.object.item_audio_file_name.blank? - %audio{ src: '' } - - else - %audio{ src: i.object.item_audio.as_json, autoplay: false, controls: true, name: 'audioQuestion', id: 'audio_item'} - %p= t("errors.messages.audio") - - - if i.object.item_audio_file_name - = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: 'remove_file_item(this, "audio")', :'data-url' => remove_audio_item_exam_question_path(i.object, question_id: i.object.question.id) - = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: "remove_file_item_tmp(this, 'audio')", class: 'trash-tmp', style: 'display: none' - - else - = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: "remove_file_item_tmp(this, 'audio')" - + - if i.object.item_audio_file_name.blank? + %audio{ src: '' } + - else + %audio{ src: i.object.item_audio.as_json, autoplay: false, controls: true, name: 'audioQuestion', id: 'audio_item'} + %p= t("errors.messages.audio") = i.input :item_audio, as: :file, label: false - = link_to t('questions.form.items.add_audio'), "#void", onclick: 'add_file(this)', class: 'btn btn_main add_file' + - if i.object.item_audio_file_name + = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: 'remove_file_item(this, "audio")', :'data-url' => remove_audio_item_exam_question_path(i.object, question_id: i.object.question.id) + = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: "remove_file_item_tmp(this, 'audio')", class: 'trash-tmp', style: 'display: none' + - else + = link_to content_tag(:i, nil, class: 'icon-trash'), "#void", onclick: "remove_file_item_tmp(this, 'audio')" + = link_to t('questions.form.items.add_audio'), "#void", onclick: 'add_file(this)', class: 'btn btn_main add_file' .info = i.input :audio_description, as: :text, label: t('questions.form.items.audio_description') - - .alt_info.title - =link_to "#void", onclick: 'toggle_alt_info(this)', onkeydown: 'click_on_keypress(event, this);' do - =raw t('questions.form.audio.alt_title') - %i.icon-arrow-down-triangle - %i.icon-arrow-up-triangle.invisible - .invisible.alt_info_text{tabindex: 0} - = i.label t('questions.form.audio.aud_desc'), class: 'alt' + = i.label t('questions.form.audio.aud_desc'), class: 'alt' -else - count += 1 @@ -114,8 +92,6 @@ var ckb = $(this).parent().find('input[type="checkbox"]'); ckb.prop('checked', !ckb.prop('checked')); }); - - hide_trash_if_empty(); }); $('#items .duplicatable_nested_form').each(function(idx) { @@ -132,43 +108,8 @@ }); expand_existing_options(); - - hide_trash_if_empty(); - - $(document).on('change', '.image input[type="file"], .audio input[type="file"]', function(){ - if($(this).val()){ - $(this).parents('.upload-preview:first').find('a:has(.icon-trash)').show(); - - var trash_tmp = $(this).parents('.upload-preview:first').find('.trash-tmp'); - - if(trash_tmp.length > 0 && trash_tmp.is(':hidden')){ - trash_tmp.show(); - } else { - $(this).parents('.upload-preview:first').find('a:has(.icon-trash)').not('.trash-tmp[style*="display: none"]').show(); - $(this).parents('.upload-preview:first').find('audio').show(); - $(this).parents('.upload-preview:first').find('img').show(); - } - } - }); }); - function hide_trash_if_empty(){ - $('.upload-preview').each(function(){ - var img = $(this).find('img'); - var audio = $(this).find('audio'); - var hasContent = false; - - - if(img.length > 0 && img.attr('src') != '' && img.attr('src') != null) hasContent = true; - - if(audio.length > 0 && audio.attr('src') != '' && audio.attr('src') != null) hasContent = true; - - if(!hasContent){ - $(this).find('a:has(.icon-trash)').hide(); - } - }); - } - function add_comment(btn){ $(btn).parents('.duplicatable_nested_form:first').find('.comment_area').fadeToggle(function(){ if ($(this).is(':visible')) @@ -204,10 +145,8 @@ function remove_file_item_tmp(link, type){ $(link).parents('.upload-preview:first').find(type).attr('src', ''); $(link).parents('.upload-preview:first').find('input.file').val(''); - $(link).parent().find(type).hide(); - $(link).hide(); - if(type === 'audio') + if(type == 'audio') $(link).parents('.upload-preview:first').parent().find('.info .text textarea').val(''); else $(link).parents('.upload-preview:first').parent().find('.info .string input').val(''); @@ -216,7 +155,7 @@ function add_image(btn){ $(btn).parents('.duplicatable_nested_form:first').find('.image').fadeToggle(function(){ if ($(this).is(':visible')) - // $(this).css('display','inline-block'); + $(this).css('display','inline-block'); $(this).toggleClass('hide'); }); } @@ -224,7 +163,7 @@ function add_audio(btn){ $(btn).parents('.duplicatable_nested_form:first').find('.audio').fadeToggle(function(){ if ($(this).is(':visible')) - // $(this).css('display','inline-block'); + $(this).css('display','inline-block'); $(this).toggleClass('hide'); }); } diff --git a/app/views/questions/form/_label.html.haml b/app/views/questions/form/_label.html.haml index 96d1514d4..c6dedca0c 100644 --- a/app/views/questions/form/_label.html.haml +++ b/app/views/questions/form/_label.html.haml @@ -1,11 +1,11 @@ -%div#labels +%span#labels - count = 0 - count_labels = f.object.question_labels.count = f.nested_fields_for :question_labels, wrapper_tag: :div do |label| - if (f.object.new_record? || label.object.persisted? || count == count_labels + 1) - count += 1 .duplicatable_nested_form - = label.input :name, label: false = label.remove_nested_fields_link content_tag(:i, nil, class: 'icon-trash') + = label.input :name, label: false -else - count += 1 \ No newline at end of file diff --git a/app/views/questions/form/_media.html.haml b/app/views/questions/form/_media.html.haml index abc9f7790..17988e28c 100644 --- a/app/views/questions/form/_media.html.haml +++ b/app/views/questions/form/_media.html.haml @@ -22,7 +22,7 @@ = render partial: 'questions/form/audio', locals: { f: f, eq: nil } .input.string = f.add_nested_fields_link :question_audios, t('.add_more_audios'), class: 'btn' -%fieldset.text_labels +%fieldset.labels %legend#associated_text_label = f.label t('.associated_text') #question_text @@ -32,8 +32,8 @@ - disabled = f.object.disabled_option(@exam_question.exam_id) ? true : nil - checked = f.object.checked_option(@exam_question.exam_id) ? false : true - #associated_radio - = f.collection_radio_buttons :question_text_id, [[true, t('questions.form.text.import')], [false, t('questions.form.text.new')]], :first, :last, checked: checked, disabled: disabled, boolean_style: :inline, item_wrapper_tag: false + #associated_radio + = f.collection_radio_buttons :question_text_id, [[true, t('questions.form.text.import')], [false, t('questions.form.text.new')]], :first, :last, checked: checked, disabled: disabled, boolean_style: :inline, item_wrapper_tag: false = render partial: 'questions/form/text', locals: { f: f, eq: nil } diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index 9cccc5c65..c976f26da 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -4608,13 +4608,11 @@ en_US: add_labels: Add labels add_audios: Add audios image: - alt_title: 'What is Alt?' - alt_desc: 'Mandatory description to accessibility (disabled people)' + alt_desc: 'Alt: Mandatory description to accessibility (disabled people)' add_image: Add/update image audio: add_audio: Add/update audio - alt_title: 'Why use Audio Description?' - aud_desc: 'If there are any hearing-impaired students in the class, please provide audio description so that they can follow the content.' + aud_desc: 'Alt: Mandatory description to accessibility (disabled people)' text: text: Text associated_text: Associated text diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index 346d14e86..da511d6d4 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -4933,13 +4933,11 @@ pt_BR: add_labels: Adicionar palavras-chave add_audios: Adicionar áudios image: - alt_title: 'O que é o Alt?' - alt_desc: 'Descrição obrigatória para acessibilidade (pessoas com deficiência)' + alt_desc: 'Alt: Descrição obrigatória para acessibilidade (pessoas com deficiência)' add_image: Adicionar/alterar imagem audio: add_audio: Adicionar/alterar áudio - alt_title: 'Por que usar a Áudio Descrição?' - aud_desc: 'Se houver algum deficiente auditivo na turma, por favor realizar a áudio descrição para que o aluno possa acompanhar o conteúdo.' + aud_desc: 'Se houver algum deficiente auditivo na turma, por favor realizar a áudio descrição de modo que este aluno não seja prejudicado.' text: text: Texto associated_text: Texto associado From 4ac298240215c1773507fc4b0c9145e510b41751 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Thu, 8 Jan 2026 13:20:32 -0300 Subject: [PATCH 54/70] chore: do not show correction_info if question_type persisted --- app/views/questions/form/_info.html.haml | 29 ++++++++++++------------ 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/app/views/questions/form/_info.html.haml b/app/views/questions/form/_info.html.haml index dbb34fa52..b3ed97f08 100644 --- a/app/views/questions/form/_info.html.haml +++ b/app/views/questions/form/_info.html.haml @@ -15,19 +15,20 @@ - unless eq.nil? = eq.input :score, input_html: { step: '0.5' } - .correction_info - .expand_correction - =link_to('#void', onclick: 'toggle_text(this);' , onkeydown: 'click_on_keypress(event, this); ') do - = t('questions.question.understanding_score_calculation') - %i.icon-arrow-down-triangle{:'aria-hidden'=>'true'} - %i.icon-arrow-up-triangle.invisible{:'aria-hidden'=>'true'} - .correction_text - .invisible.true_or_false_calculation - = raw(t('questions.types.true_or_false_calculation', score_item: t('questions.question.total_amount'))) - .invisible.unique_choice_calculation - = raw(t('questions.types.unique_choice_calculation', score_item: t('questions.question.total_amount_per_items'))) - .invisible.multiple_choice_new_calculation - = raw(t('questions.types.multiple_choice_new_calculation', score_item: t('questions.question.total_amount_per_items'))) + -unless f.object.persisted? + .correction_info + .expand_correction + =link_to('#void', onclick: 'toggle_text(this);' , onkeydown: 'click_on_keypress(event, this); ') do + = t('questions.question.understanding_score_calculation') + %i.icon-arrow-down-triangle{:'aria-hidden'=>'true'} + %i.icon-arrow-up-triangle.invisible{:'aria-hidden'=>'true'} + .correction_text + .invisible.true_or_false_calculation + = raw(t('questions.types.true_or_false_calculation', score_item: t('questions.question.total_amount'))) + .invisible.unique_choice_calculation + = raw(t('questions.types.unique_choice_calculation', score_item: t('questions.question.total_amount_per_items'))) + .invisible.multiple_choice_new_calculation + = raw(t('questions.types.multiple_choice_new_calculation', score_item: t('questions.question.total_amount_per_items'))) - unless only_points .labels @@ -110,7 +111,7 @@ if("#{f.object.have_media?}"=='false'){ $('.medias').hide(); $("#go_to").attr("onclick","go_to_items()"); - //$("#question_texts_media_question").attr("checked","false"); + $("#question_texts_media_question").attr("checked","false"); }else{ $('.medias').show(); From 9d2e4ccef561537efc5f9e855cf48b3c73bb5fc5 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Fri, 9 Jan 2026 09:09:49 -0300 Subject: [PATCH 55/70] fix: solve error when question was copied only to the repository --- app/assets/javascripts/questions.js.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index 5d32234e2..570329a3b 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -283,7 +283,7 @@ function edit_question(icon, replace_list){ var url_copy; if (data.action === 'copy_repository') { - url_copy = "<%=Rails.application.routes.url_helpers.copy_question_path(':id')%>".replace(':id', ids); + url_copy = "<%=Rails.application.routes.url_helpers.copy_question_path(':id')%>".replace(':id', questions_ids); } else { url_copy = $(icon).prev().data('link-to-copy').replace(':id', ids) + (data.action === 'fork_copy' ? "?fork_copy=true" : ""); } From 9d049bc0344a494628caa26cda24d1e70a5754fc Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Mon, 12 Jan 2026 11:03:53 -0300 Subject: [PATCH 56/70] chore: adjust exam_question can_save? function --- app/models/exam.rb | 7 +++++-- app/models/exam_question.rb | 6 +++--- app/models/question.rb | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/models/exam.rb b/app/models/exam.rb index f882c9b3e..4e69aa928 100644 --- a/app/models/exam.rb +++ b/app/models/exam.rb @@ -524,13 +524,12 @@ def get_duration(total_time) # Return true if the exam is in draft mode, has not started, or started without ACU, and the user is the editor of the class where the exam is being used. # Return false if exam is published and closed, or started with ACU, or started without ACU and user is not a class editor. def can_questions_be_edited?(current_user, is_exam_question = false) - has_acu = academic_allocation_users.any? allocation_tags = academic_allocations.pluck(:allocation_tag_id) is_editor = is_exam_question ? true : current_user.profiles_with_access_on('update', 'questions', allocation_tags, true).any? return true unless status return true unless started? - is_editor && !has_acu + is_editor && !has_acu? end def notify_responsibles(message) @@ -540,6 +539,10 @@ def notify_responsibles(message) Allocation.send_email_to_responsible_users(allocation_tags, subject, message_body) end + def has_acu? + academic_allocation_users.any? + end + def current_time_db result = ActiveRecord::Base.connection.execute("SELECT current_time AS current_time") return Time.parse(result.to_a[0]['current_time']) diff --git a/app/models/exam_question.rb b/app/models/exam_question.rb index 37ae3b2bb..0d2557cd2 100644 --- a/app/models/exam_question.rb +++ b/app/models/exam_question.rb @@ -119,20 +119,20 @@ def log_description end def can_save? - raise 'cant_change_after_published' if exam.status && new_record? + raise 'cant_change_after_published' if exam.has_acu? && (!question.privacy || question.owner? ) end def unpublish exam.update_attributes status: false end - def each_item_score + def each_item_score item_score = case question.type_question when Question::UNIQUE score when Question::MULTIPLE (score/question_items.where(value: true).count).round(2) - else + else (score/question_items.count).round(2) end diff --git a/app/models/question.rb b/app/models/question.rb index eb3fe8f50..0c58ae736 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -318,7 +318,7 @@ def can_import_or_export?(current_user, exam = nil) def can_copy? owners = owners? - raise 'private' unless (!privacy) || owners + raise 'private' unless owners || !privacy raise 'draft' if (!privacy && !status) && !owners end From 865b5b1a393c23e7c5482eb2f535267c7488c191 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Mon, 12 Jan 2026 11:04:31 -0300 Subject: [PATCH 57/70] feat: adjust fork_copy implementation --- app/assets/javascripts/questions.js.erb | 6 +- app/controllers/exam_questions_controller.rb | 78 +++++++++++++++----- config/routes.rb | 1 + 3 files changed, 64 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index 570329a3b..47a2f80b8 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -284,8 +284,10 @@ function edit_question(icon, replace_list){ if (data.action === 'copy_repository') { url_copy = "<%=Rails.application.routes.url_helpers.copy_question_path(':id')%>".replace(':id', questions_ids); + } else if (data.action === 'fork_copy') { + url_copy = "<%=Rails.application.routes.url_helpers.fork_copy_exam_question_path(':id')%>".replace(':id', ids); } else { - url_copy = $(icon).prev().data('link-to-copy').replace(':id', ids) + (data.action === 'fork_copy' ? "?fork_copy=true" : ""); + url_copy = $(icon).prev().data('link-to-copy').replace(':id', ids); } $(icon).prev().call_fancybox({ @@ -299,7 +301,7 @@ function edit_question(icon, replace_list){ beforeClose: function() { clear_ckeditor(); } }); - if((replace_list !== '') && (data.action !== 'fork_copy')){ + if(replace_list !== ''){ $.get($(replace_list).data("link-list"), function(data2){ $(replace_list).replaceWith(data2); flash_message(data.notice, 'notice'); diff --git a/app/controllers/exam_questions_controller.rb b/app/controllers/exam_questions_controller.rb index 58796f405..42ca3918e 100644 --- a/app/controllers/exam_questions_controller.rb +++ b/app/controllers/exam_questions_controller.rb @@ -22,9 +22,17 @@ def new end def create + @forked_question = ExamQuestion.find(params[:forked_question_id]) @exam_question = ExamQuestion.new exam_question_params + authorize! :create, Question, { on: at_ids = @exam_question.exam.allocation_tags.map(&:id) } - @exam_question.question.user_id = current_user.id + if @forked_question.present? + @exam_question.question.user_id = @forked_question.question.user_id + @exam_question.question.updated_by_user_id = current_user.id + @exam_question.question.status = true + else + @exam_question.question.user_id = current_user.id + end ActiveRecord::Base.transaction do if !params['question_texts']['text'].strip.blank? @@ -37,6 +45,13 @@ def create end if @exam_question.save + + if @forked_question.present? + @forked_question.destroy + + log(@forked_question.exam, "question: #{@forked_question.question.id} [moved/deleted after copy] exam: #{@forked_question.exam.id}", LogAction::TYPE[:update]) rescue nil + end + if @exam_question.exam.questions.size > 1 render partial: 'question', locals: { question: @exam_question.question, exam_question: @exam_question, exam: @exam_question.exam, hide_columns: false, can_see_preview: true } else @@ -97,15 +112,6 @@ def update end end - if params[:forked_question_id].present? - forked_question = ExamQuestion.find(params[:forked_question_id]) - Rails.logger.info "\n\n\n exam_question.id #{@exam_question.question.id} \n\n\n" - Rails.logger.info "\n\n\n forked_question.id #{forked_question.question.id} \n\n\n" - forked_question.destroy - - log(forked_question.exam, "question: #{forked_question.question.id} [moved/deleted after copy] exam: #{forked_question.exam.id}", LogAction::TYPE[:update]) rescue nil - end - if !params['question_texts_id'].blank? && params['question_texts']['media_question'].to_i == 0 if Question.where(:question_text_id => params['question_texts_id']).count == 0 QuestionText.find(params['question_texts_id']).destroy @@ -128,6 +134,28 @@ def update render_json_error(error, 'questions.error') end + def fork_copy + authorize! :copy, Question + + original_exam_question = ExamQuestion.find params[:id] + original_question = original_exam_question.question + original_question.can_copy? + + @new_question = original_question.dup + + copy_exam_question_associations(original_question, @new_question) + + @exam_question = ExamQuestion.new(original_exam_question.attributes.except('id', 'question_id', 'annulled').merge({ annulled: false })) + @exam_question.question = @new_question + @forked_question = original_exam_question + + render :new + rescue CanCan::AccessDenied + render text: t(:no_permission) + rescue => error + render text: t("exam_questions.errors.#{error}") + end + def remove_file_item authorize! :update, Question @@ -377,18 +405,11 @@ def export def copy authorize! :copy, Question exam_question = ExamQuestion.find params[:id] - is_fork_copy = params[:fork_copy].nil? ? false : true - exam_question.can_save? exam_question.question.can_copy? - @exam_question = ExamQuestion.copy(exam_question, current_user.id, is_fork_copy) - @forked_question = exam_question - - Rails.logger.info "\n\n\n exam_question.id #{@exam_question.question.id} \n\n\n" - Rails.logger.info "\n\n\n forked_question.id #{@forked_question.question.id} \n\n\n" + @exam_question = ExamQuestion.copy(exam_question, current_user.id) - - log(ExamQuestion.exam, "question: #{exam_question.question_id} #{ '[copy]' }, #{exam_question.log_description}", LogAction::TYPE[:create]) rescue nil + log(ExamQuestion.exam, "question: #{exam_question.question_id} [copy], #{exam_question.log_description}", LogAction::TYPE[:create]) rescue nil build_exam_question @@ -397,6 +418,7 @@ def copy render text: t(:no_permission) rescue => error render text: t("exam_questions.errors.#{error}") + # render_json_error(error, 'exam_questions.errors') end private @@ -424,6 +446,24 @@ def log(object, message, type=LogAction::TYPE[:update]) end end + def copy_exam_question_associations(source_question, target_question) + source_question.question_items.each do |item| + target_question.question_items << item.dup + end + + source_question.question_images.each do |img| + target_question.question_images << img.dup + end + + source_question.question_labels.each do |label| + target_question.question_labels << label.dup + end + + source_question.question_audios.each do |audio| + target_question.question_audios << audio.dup + end + end + def build_exam_question if @exam_question.question.nil? @exam_question.build_question do |q| diff --git a/config/routes.rb b/config/routes.rb index 17f7d2723..75bf57232 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -707,6 +707,7 @@ get '/export/exam_questions/steps', to: 'exam_questions#export_steps', as: :export_steps put :publish get :copy + get :fork_copy put :remove_image_item, to: 'exam_questions#remove_file_item', type: 'image' put :remove_audio_item, to: 'exam_questions#remove_file_item', type: 'audio' end From 0e6f87ab1ec457b61a1e1d9a0d04f114e27233b9 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Tue, 13 Jan 2026 12:22:53 -0300 Subject: [PATCH 58/70] chore: minor adjusts in alert messages --- app/assets/javascripts/questions.js.erb | 4 +++- config/locales/en_US.yml | 5 +++-- config/locales/pt_BR.yml | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index 47a2f80b8..57d3697f3 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -269,7 +269,9 @@ function edit_question(icon, replace_list){ if (confirm(data.alert)) { data.action = 'only_points'; } else { - data.action = 'fork_copy'; + if (confirm("<%=I18n.t('questions.error.fork_copy')%>")) { + data.action = 'fork_copy'; + } } } diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index c976f26da..1263841c4 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -4631,9 +4631,10 @@ en_US: in_use_question: "This question is being used on a started test. Would you like to duplicate it for editing?" in_use_exam_question: "This question is being used on another exam. Would you like to duplicate it for editing?" not_owner: "Only the author can edit the question. Do you want to make a copy?" - copy_repository: "This question is in a test that has already begun. Do you want to create a copy in the repository?" + copy_repository: "This question is in a test that has already begun or ended. Do you want to create a copy in the repository?" only_points: "You are not authorized to edit the content of this question. Do you want to change the score of the selected question on the exam?" - can_fork_copy: "Do you want to change the score of the selected question on the exam? Click 'OK' to confirm and 'Cancel' to continue editing." + can_fork_copy: "Do you want to change the score of the selected question on the exam?" + fork_copy: "Do you want to proceed to the editing screen?" privacy_in_use: "can't be defined as private, because is being used at exams. If you're the author and the question is in use only at your exams, we suggest that you remove it from the exam and change it at Repositories area. If the question is in use at others exams that are not yours, it's not possible to change it." privacy_permission: can't be defined as private. You don't have permission to execute this action. min_items: The question must have, at least, 3 items diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index da511d6d4..dd3f775e5 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -4956,9 +4956,10 @@ pt_BR: in_use_question: "Essa questão está sendo utilizada em uma prova já iniciada. Deseja duplicá-la para fazer a edição?" in_use_exam_question: "Essa questão está sendo utilizada em outra prova. Deseja duplicá-la para fazer a edição?" not_owner: "Somente o autor pode editar a questão. Deseja fazer uma cópia?" - copy_repository: "Está questão está em uma prova já iniciada. Deseja criar uma cópia no repositório?" + copy_repository: "Está questão está em uma prova já iniciada ou encerrada. Deseja criar uma cópia no repositório?" only_points: "Você não tem autorização para editar o conteúdo desta questão. Deseja alterar a pontuação da questão selecionada na prova?" - can_fork_copy: "Deseja alterar a pontuação da questão selecionada na prova? Clique em 'Ok' para confirmar e 'Cancelar' para continuar com a edição." + can_fork_copy: "Deseja alterar somente a pontuação da questão selecionada na prova?" + fork_copy: "Deseja seguir para a tela de edição?" privacy_in_use: "não pode ser definida como privada, pois está sendo usada em provas. Se você é o autor e a questão está em uso apenas em sua prova, sugerimos que remova-a de sua prova e se dirija ao repositório para alterá-la. Caso a questão esteja em uso em outras provas não criadas por você, não é possível alterá-la" privacy_permission: não pode ser definida como privada. Você não tem permissão para realizar esta ação. min_items: A questão precisa ter, ao menos, 3 itens From 68e4c592dbf3c8e7b31b4fe44bdfa9aa3307c913 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Tue, 13 Jan 2026 12:26:33 -0300 Subject: [PATCH 59/70] chore: adjusts exam_question.rb verifications --- app/models/exam_question.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/exam_question.rb b/app/models/exam_question.rb index 0d2557cd2..6402a95c6 100644 --- a/app/models/exam_question.rb +++ b/app/models/exam_question.rb @@ -98,7 +98,7 @@ def set_order end def can_reorder? - raise 'already_started' if exam.status && exam.on_going? + raise 'already_started' if exam.on_going? && exam.has_acu? end def can_change_annulled? @@ -123,7 +123,7 @@ def can_save? end def unpublish - exam.update_attributes status: false + exam.update_attributes status: false unless can_reorder? end def each_item_score From 184023e5428ce73b4401688950745e8aae22dbc4 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Tue, 13 Jan 2026 12:28:20 -0300 Subject: [PATCH 60/70] fix: fix error when create new exam_question --- app/controllers/exam_questions_controller.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/controllers/exam_questions_controller.rb b/app/controllers/exam_questions_controller.rb index 42ca3918e..b7fbd3426 100644 --- a/app/controllers/exam_questions_controller.rb +++ b/app/controllers/exam_questions_controller.rb @@ -22,13 +22,13 @@ def new end def create - @forked_question = ExamQuestion.find(params[:forked_question_id]) @exam_question = ExamQuestion.new exam_question_params - authorize! :create, Question, { on: at_ids = @exam_question.exam.allocation_tags.map(&:id) } - if @forked_question.present? + + if params[:forked_question_id].present? + @forked_question = ExamQuestion.find(params[:forked_question_id]) @exam_question.question.user_id = @forked_question.question.user_id - @exam_question.question.updated_by_user_id = current_user.id + @exam_question.question.updated_by_user_id = current_user.id unless current_user.id == @forked_question.question.user_id @exam_question.question.status = true else @exam_question.question.user_id = current_user.id @@ -47,9 +47,8 @@ def create if @exam_question.save if @forked_question.present? - @forked_question.destroy - log(@forked_question.exam, "question: #{@forked_question.question.id} [moved/deleted after copy] exam: #{@forked_question.exam.id}", LogAction::TYPE[:update]) rescue nil + @forked_question.destroy end if @exam_question.exam.questions.size > 1 From 37602918ad13a8c133ee91c820f24365f9c73c6f Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 14 Jan 2026 09:25:25 -0300 Subject: [PATCH 61/70] chore: minor layout adjusts --- app/assets/javascripts/questions.js.erb | 19 +++++++++++++++++++ app/views/questions/form/_image.html.haml | 3 --- app/views/questions/form/_info.html.haml | 2 +- app/views/questions/form/_item.html.haml | 18 ------------------ 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index 57d3697f3..933545007 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -175,6 +175,7 @@ function back(){ $.fancybox.update(); $('.block_content').css('width', 'auto'); } + function back_medias(){ go_to_medias(); } @@ -459,6 +460,24 @@ function expand_or_compress_icon(icon){ } } +function toggle_alt_info(link){ + var parent = $(link).parent(); + parent.siblings().focus(); + var information = parent.siblings('.alt_info_text') + + if(information.hasClass('invisible')){ + information.removeClass('invisible') + parent.find('i.icon-arrow-up-triangle').removeClass('invisible'); + parent.find('i.icon-arrow-down-triangle').addClass('invisible'); + }else{ + information.addClass('invisible') + parent.find('i.icon-arrow-up-triangle').addClass('invisible'); + parent.find('i.icon-arrow-down-triangle').removeClass('invisible'); + } + + $(link).focus(); +} + function annul(icon){ if ($(icon).prop('disabled')) return false; diff --git a/app/views/questions/form/_image.html.haml b/app/views/questions/form/_image.html.haml index cd8833167..5b0b9e81e 100644 --- a/app/views/questions/form/_image.html.haml +++ b/app/views/questions/form/_image.html.haml @@ -39,7 +39,4 @@ $(btn).parent().find('input.file').click(); } - function toggle_alt_info(link){ - $(link).siblings().find('.alt_info_text').toggle('invisible'); - } diff --git a/app/views/questions/form/_info.html.haml b/app/views/questions/form/_info.html.haml index 69b8511c1..c334b47ba 100644 --- a/app/views/questions/form/_info.html.haml +++ b/app/views/questions/form/_info.html.haml @@ -120,7 +120,7 @@ $("#question_texts_media_question").attr("checked","true"); } - $('#exam_question_question_attributes_type_question')[0].addEventListener("change", (event) => { + $('#exam_question_question_attributes_type_question, #question_type_question')[0].addEventListener("change", (event) => { if ($('.correction_text').children(':visible').length>0){ $('.expand_correction a').click(); } diff --git a/app/views/questions/form/_item.html.haml b/app/views/questions/form/_item.html.haml index 3e32c46d6..cd7cc4f29 100644 --- a/app/views/questions/form/_item.html.haml +++ b/app/views/questions/form/_item.html.haml @@ -247,21 +247,3 @@ } }); } - - function toggle_alt_info(link){ - var parent = $(link).parent(); - parent.siblings().focus(); - var information = parent.siblings('.alt_info_text') - - if(information.hasClass('invisible')){ - information.removeClass('invisible') - parent.find('i.icon-arrow-up-triangle').removeClass('invisible'); - parent.find('i.icon-arrow-down-triangle').addClass('invisible'); - }else{ - information.addClass('invisible') - parent.find('i.icon-arrow-up-triangle').addClass('invisible'); - parent.find('i.icon-arrow-down-triangle').removeClass('invisible'); - } - - $(link).focus(); - } From b25a4a917bf8604b26c770b0704d80d701eaa250 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 14 Jan 2026 10:53:08 -0300 Subject: [PATCH 62/70] chore: add verification in question in ended exams --- app/models/exam.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/exam.rb b/app/models/exam.rb index 4e69aa928..e50c41b65 100644 --- a/app/models/exam.rb +++ b/app/models/exam.rb @@ -527,6 +527,7 @@ def can_questions_be_edited?(current_user, is_exam_question = false) allocation_tags = academic_allocations.pluck(:allocation_tag_id) is_editor = is_exam_question ? true : current_user.profiles_with_access_on('update', 'questions', allocation_tags, true).any? + return false if ended? return true unless status return true unless started? is_editor && !has_acu? From df01a72f477aba102fb3436fc02c9db4f54833ba Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 14 Jan 2026 10:53:54 -0300 Subject: [PATCH 63/70] chore: adjust cancel button response in can_fork_copy data.action --- app/assets/javascripts/questions.js.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/questions.js.erb b/app/assets/javascripts/questions.js.erb index 933545007..29ab40477 100644 --- a/app/assets/javascripts/questions.js.erb +++ b/app/assets/javascripts/questions.js.erb @@ -273,6 +273,7 @@ function edit_question(icon, replace_list){ if (confirm("<%=I18n.t('questions.error.fork_copy')%>")) { data.action = 'fork_copy'; } + return this; } } From b0b999fd86983b3ce29ffcc3e131b4b9d6c44c0c Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 14 Jan 2026 10:55:37 -0300 Subject: [PATCH 64/70] chore: correct verification when the question has many exams and it is in exam_question view --- app/models/question.rb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/models/question.rb b/app/models/question.rb index 0c58ae736..deade4924 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -254,12 +254,13 @@ def have_access? # Return :only_points if only the points can be edited # Raise an exception if the question cannot be edited def verify_edit_permission(current_user, exam_id=nil) - has_blocking_exam = exams.any? do |exam| - next false if exam.nil? - !exam.can_questions_be_edited?(current_user, exam.id == exam_id) - end if exam_id.nil? + has_blocking_exam = exams.any? do |exam| + next false if exam.nil? + !exam.can_questions_be_edited?(current_user) + end + raise 'in_use' if !owner? && privacy return :copy_blocked_exam if has_blocking_exam @@ -268,15 +269,16 @@ def verify_edit_permission(current_user, exam_id=nil) else has_access = owner? || !privacy n_exams = exams.ids.length > 1 + can_be_edited = Exam.find(exam_id).can_questions_be_edited?(current_user, true) - if has_blocking_exam + unless can_be_edited return :copy_repository if n_exams || has_access raise 'in_use' end - return (has_access ? :fork_copy : :only_points) if n_exams + return :only_points unless has_access + return :fork_copy if n_exams return :edit_and_notify if owner? - return :only_points if privacy :can_fork_copy end end From 229667d4f08a2823ec39d0e439f49376c68ebd61 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 14 Jan 2026 12:14:14 -0300 Subject: [PATCH 65/70] chore: remove unused code --- app/controllers/exam_questions_controller.rb | 2 +- app/controllers/questions_controller.rb | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/controllers/exam_questions_controller.rb b/app/controllers/exam_questions_controller.rb index b7fbd3426..3b4656b8c 100644 --- a/app/controllers/exam_questions_controller.rb +++ b/app/controllers/exam_questions_controller.rb @@ -83,7 +83,7 @@ def update authorize! :update, Question @exam_question = ExamQuestion.find params[:id] - if params['question_texts'].present? && params['question_texts']['media_question'].to_i == 1 + if params['question_texts']['media_question'].to_i == 1 if !params['question_texts_id'].blank? @question_text = QuestionText.find(params['question_texts_id']) diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index dd82f9838..47ede539d 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -86,14 +86,10 @@ def update authorize! :update, Question @question = Question.find params[:id] - text_changed = false - if !params['question_texts']['text'].blank? - @question_text = @question.question_text || @question.build_question_text - @question_text.text = params['question_texts']['text'] - if @question_text.save - text_changed = @question_text.previous_changes.present? - end + @question.question_text.text = params['question_texts']['text'] + @question_text = @question.question_text + @question_text.save end if @question.update_attributes question_params From 4810b5c4107de1a5e5835fdb2d3cc379a0a6f2fc Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Wed, 14 Jan 2026 16:07:45 -0300 Subject: [PATCH 66/70] chore: adjust raise message --- config/locales/en_US.yml | 3 ++- config/locales/pt_BR.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index d572c2b4c..d7fdadb2a 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -4634,7 +4634,8 @@ en_US: in_use: "This question is being used on exams. We suggest you make a copy so you can modify it." in_use_question: "This question is being used on a started test. Would you like to duplicate it for editing?" in_use_exam_question: "This question is being used on another exam. Would you like to duplicate it for editing?" - not_owner: "Only the author can edit the question. Do you want to make a copy?" + copy_ownership: "Only the author can edit the question. Do you want to make a copy?" + not_owner: "Only the author can edit the question. We suggest you make a copy so you can modify it." copy_repository: "This question is in a test that has already begun or ended. Do you want to create a copy in the repository?" only_points: "You are not authorized to edit the content of this question. Do you want to change the score of the selected question on the exam?" can_fork_copy: "Do you want to change the score of the selected question on the exam?" diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index d44a65994..bc06b40a3 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -4958,7 +4958,8 @@ pt_BR: in_use: "Essa questão está sendo utilizada em provas. Sugerimos que faça uma cópia para que possa modificá-la." in_use_question: "Essa questão está sendo utilizada em uma prova já iniciada. Deseja duplicá-la para fazer a edição?" in_use_exam_question: "Essa questão está sendo utilizada em outra prova. Deseja duplicá-la para fazer a edição?" - not_owner: "Somente o autor pode editar a questão. Deseja fazer uma cópia?" + copy_ownership: "Somente o autor pode editar a questão. Deseja fazer uma cópia?" + not_owner: "Somente o autor pode editar a questão. Sugerimos que faça uma cópia para que possa modificá-la." copy_repository: "Está questão está em uma prova já iniciada ou encerrada. Deseja criar uma cópia no repositório?" only_points: "Você não tem autorização para editar o conteúdo desta questão. Deseja alterar a pontuação da questão selecionada na prova?" can_fork_copy: "Deseja alterar somente a pontuação da questão selecionada na prova?" From b081d165f3bedf464ade4412d9a532e893729c31 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Thu, 15 Jan 2026 08:42:38 -0300 Subject: [PATCH 67/70] chore: add new raise message to verify_edit_permission function --- app/models/question.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/question.rb b/app/models/question.rb index deade4924..6ed8d81ff 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -261,7 +261,7 @@ def verify_edit_permission(current_user, exam_id=nil) !exam.can_questions_be_edited?(current_user) end - raise 'in_use' if !owner? && privacy + raise 'not_owner' if !owner? && privacy return :copy_blocked_exam if has_blocking_exam return :copy_ownership unless owner? @@ -273,7 +273,7 @@ def verify_edit_permission(current_user, exam_id=nil) unless can_be_edited return :copy_repository if n_exams || has_access - raise 'in_use' + raise 'not_owner' end return :only_points unless has_access From 90439e75770f7a02ec785dbee63ea5e1a0443cde Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Thu, 15 Jan 2026 10:42:27 -0300 Subject: [PATCH 68/70] refactor: adjust edition permission in exams screen --- app/models/question.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/question.rb b/app/models/question.rb index 6ed8d81ff..9a628f27d 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -272,7 +272,7 @@ def verify_edit_permission(current_user, exam_id=nil) can_be_edited = Exam.find(exam_id).can_questions_be_edited?(current_user, true) unless can_be_edited - return :copy_repository if n_exams || has_access + return :copy_repository if has_access raise 'not_owner' end From ead72911994de8a2735e12423cee799c99a39fd9 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Thu, 15 Jan 2026 14:15:17 -0300 Subject: [PATCH 69/70] fix: remove unused variable --- app/controllers/questions_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 47ede539d..0d2a82612 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -93,7 +93,7 @@ def update end if @question.update_attributes question_params - if params[:notify_responsibles] == 'true' && (text_changed || @question.previous_changes.present?) + if params[:notify_responsibles] == 'true' && @question.previous_changes.present? message = t('questions.questions.notification', question_enunciation: @question.enunciation) @question.exams.each do |exam| exam.notify_responsibles(message) @@ -221,7 +221,7 @@ def get_edit_permissions_alert_key(status) when :copy_blocked_exam 'in_use_question' when :copy_ownership - 'not_owner' + 'copy_ownership' when :copy_repository 'copy_repository' when :only_points From 4972383fdf5fa26959006d12f608817667d0aec6 Mon Sep 17 00:00:00 2001 From: mirele-ufc Date: Tue, 20 Jan 2026 14:15:04 -0300 Subject: [PATCH 70/70] feat: add internationalization to score fancybox title --- app/views/exam_questions/_steps.html.haml | 2 +- config/locales/en_US.yml | 1 + config/locales/pt_BR.yml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/exam_questions/_steps.html.haml b/app/views/exam_questions/_steps.html.haml index baeb1c55f..84c986e3e 100644 --- a/app/views/exam_questions/_steps.html.haml +++ b/app/views/exam_questions/_steps.html.haml @@ -12,7 +12,7 @@ %ul %li.info.active - if @only_points.present? - = 'Pontuação' + = t('questions.form.steps.score') -else = t('questions.form.steps.info') .dot.active#dot-info diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index d7fdadb2a..ecb627fe9 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -4578,6 +4578,7 @@ en_US: new: New question edit: Edit question medias: Images, audios or texts + score: Score items: answer: 'Answer*' add: Add more options diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index bc06b40a3..ad8327513 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -4903,6 +4903,7 @@ pt_BR: new: Nova questão edit: Editar questão medias: Imagens, áudios ou textos + score: Pontuação items: answer: 'Resposta*' add: Adicionar mais opções