Skip to content
Open
89 changes: 78 additions & 11 deletions app/assets/javascripts/class_enrollments.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,91 @@ $(function() {
$(document).on('change', '.disapproved_by_absence-input', function() {
if(this.checked){
var course_has_grade = this.getAttribute("course_has_grade");
var target_row = $(this).closest("tr");
if (course_has_grade == "true") {
var grade_of_disapproval_for_absence = this.getAttribute("grade_of_disapproval_for_absence");
grade_of_disapproval_for_absence = grade_of_disapproval_for_absence.replace(/\s+/g, '').replace(",", ".");
if (grade_of_disapproval_for_absence != "") {
grade_of_disapproval_for_absence = parseFloat(grade_of_disapproval_for_absence);
var class_enrollments_id = this.getAttribute("class_enrollments_id");
var grade = document.getElementById( "record_grade_" + class_enrollments_id ).value;
grade = grade.replace(/\s+/g, '').replace(",", ".");
if (grade == "") {
document.getElementById("record_grade_" + class_enrollments_id).value = grade_of_disapproval_for_absence;
} else {
grade = parseFloat(grade);
if( grade > grade_of_disapproval_for_absence ){
document.getElementById("record_grade_" + class_enrollments_id).value = grade_of_disapproval_for_absence;
var grade = target_row.find(".grade-input").val();
if (grade == "" || parseFloat(grade)== 0){
target_row.find(".grade-input").val(grade_of_disapproval_for_absence).trigger("input");
}
else if (parseFloat(grade) != parseFloat(grade_of_disapproval_for_absence)){
msg = this.getAttribute('overwrite_confirm_msg').replace('actual_grade',grade.replace('.',',')).replace('grade_for_disapproval',grade_of_disapproval_for_absence.replace('.',','));
if(confirm(msg)){
target_row.find(".grade-input").val(grade_of_disapproval_for_absence).trigger("input");
}
else{
this.checked = false;
}
}
}
}
else{
var disapproved_label = this.getAttribute('data_disapproved');
changeSituationInput(target_row,disapproved_label);
}
}
});
function changeSituationInput(context,situation){
var row = context.closest('tr');
var situationSelect = row.find('.situation-input');
if(situationSelect.val() != situation){
situationSelect.val(situation).trigger('change');
}
}
$(document).on('focus','.grade-input',function(){
placeholder = this.getAttribute('grade_placeholder');
$(this).attr('placeholder',placeholder);
});
$(document).on('blur','.grade-input',function(){
$(this).attr('placeholder','');
});
$(document).on('input', '.grade-input', function() {
var digits = $(this).val().replace(/\D/g, '');
if (digits === '') return;
var number = parseInt(digits, 10);
var formatted = (number / 10).toFixed(1).replace('.', ',');
$(this).val(formatted);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O cursor na nota está um pouco estranho: se você começa a digitar um valor (exemplo 6,5) e percebe que errou o primeiro dígito, ao apagar o primeiro dígito (por exemplo, pra digitar 7,5), o cursor é movido para o fim (nesse exemplo, a nota fica 5,7).

Imagino que o motivo seja essa linha substituindo o valor pelo valor formatado sem considerar a posição do cursor

var minimum_grade = this.getAttribute('minimum_grade_for_approval');
var actual_grade = parseFloat(formatted.replace(',','.'));
if(actual_grade <= 10){ /* sanity check */
if(actual_grade >= minimum_grade){
var approved_label = this.getAttribute('data_approved');
changeSituationInput($(this),approved_label);
}
else{
var disapproved_label = this.getAttribute('data_disapproved');
changeSituationInput($(this),disapproved_label);
}
}
});
$(document).on('keydown','.grade-input',function(e){
if (e.which == 40 || e.which == 38){
var subform = $(this).closest('.class_enrollments-sub-form');
var gradeInputs = subform.find('.grade-input:visible');
var index = gradeInputs.index(this);
var nextIndex = (e.which==40) ? index + 1 : index - 1;
if(nextIndex >= 0 && nextIndex < gradeInputs.length){
var nextEl = gradeInputs.eq(nextIndex);
nextEl.focus();
/* 10ms timeout to make sure the select() works properly */
setTimeout(function(){
nextEl.select();
}, 10);
}
}
});
$(document).on('as:action_success', function() {
$('.class_enrollments-sub-form').each(function() {
var subform = $(this);
if (subform.find('[course_has_grade="false"]').length>0){

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acho que seria melhor esconder o campo pelo helper do rails, ao invés de javascript. Tenho a impressão de que ficaria mais consistente com outras configurações semelhantes no projeto e com mais garantia de esconder em todo lugar que venha a incluir subform class_enrollment.

(mas não vejo problema manter aqui, se for complicado fazer por lá)

subform.addClass('hide-score-column');
}
else{
subform.find('.grade-input').val(function(i,value){
return value.replace('.',',');
});
}
});
});
});
7 changes: 7 additions & 0 deletions app/assets/stylesheets/active_scaffold_overrides.scss
Original file line number Diff line number Diff line change
Expand Up @@ -351,4 +351,11 @@ small._default_value {

.active-scaffold li.form-element dd {
padding: 6px 0;
}
.hide-score-column .grade-column,
.hide-score-column .grade_not_count_in_gpr-column{
display: none !important;
}
.grade-input{
text-align: right;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Não sei se o motivo é esse e é detalhe, mas o campo de Nota na tela de Inscrições ficou com o valor ligeiramente cortado:
image

Acredito que precise adicionar algum padding pra resolver.

}
2 changes: 2 additions & 0 deletions app/controllers/class_enrollments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class ClassEnrollmentsController < ApplicationController
format: :i18n_number,
i18n_options: { format_as: "grade" }
}
config.columns[:grade].css_class = "grade-column"
config.columns[:grade_not_count_in_gpr].css_class = "grade_not_count_in_gpr-column"
config.columns[:situation].form_ui = :select
config.columns[:situation].options = {
options: ClassEnrollment::SITUATIONS,
Expand Down
25 changes: 25 additions & 0 deletions app/controllers/concerns/shared_xls_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,29 @@ def scholarship_status(class_enrollment)
key = class_enrollment.enrollment.has_active_scholarship_now? ? "active_scholarship_true" : "active_scholarship_false"
I18n.t("xls_content.course_class.summary.#{key}")
end

def parse_grades_xlsx(file)
raise ArgumentError, "Invalid upload" unless file.is_a?(ActionDispatch::Http::UploadedFile)

original_filename = file.original_filename.to_s
valid_filename = original_filename.end_with?(".xlsx") &&
!original_filename.include?("/") &&
!original_filename.include?("\\")
raise ArgumentError, "Invalid file name" unless valid_filename

grades = {}
Zip::File.open(file.tempfile.path) do |zip|
sheet = zip.find_entry("xl/worksheets/sheet1.xml")
xml = Nokogiri::XML(sheet.get_input_stream.read)
ns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
xml.xpath("//xmlns:row", "xmlns" => ns).each_with_index do |row, index|
next if index == 0
enrollment_number = row.at_xpath('xmlns:c[@r[starts-with(., "B")]]//xmlns:v', "xmlns" => ns)&.text

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Será que não tem uma forma mais legível de definir quais são as colunas usadas?

grade_node = row.at_xpath('xmlns:c[@r[starts-with(., "E")]]//xmlns:v', "xmlns" => ns)
next if enrollment_number.nil?
grades[enrollment_number] = grade_node&.text
end
end
grades
end
end
32 changes: 32 additions & 0 deletions app/controllers/course_classes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ class CourseClassesController < ApplicationController
page: true,
type: :member,
parameters: { format: :xlsx }
config.action_links.add "import_grades_xls",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Essa ação deveria entrar no ability para permitir que professor use a funcionalidade também. Por enquanto, só está habilitada pra admin

label: "<i title='Importar Notas' class='fa fa-upload'></i>".html_safe,
type: :member,
position: :replace,
crud_type: :update


config.list.sorting = { name: "ASC", id: "DESC" }
Expand Down Expand Up @@ -150,6 +155,33 @@ def summary_xls
end
end

def import_grades_xls
@course_class = CourseClass.find(params[:id])
if request.post? && params[:spreadsheet].present?
file = params[:spreadsheet]
minimum_grade_for_approval = CustomVariable.minimum_grade_for_approval
grades = parse_grades_xlsx(file)
grades.each do |id, grade|
enrollment = Enrollment.find_by(enrollment_number: id)
class_enrollment = @course_class.class_enrollments.find_by(enrollment: enrollment)
if class_enrollment
class_enrollment.grade = grade
if class_enrollment.grade.to_i >= minimum_grade_for_approval

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nesse if só está considerando a nota para definir Aprovado/Reprovado, mas seria bom considerar Freq S/I para reprovação por falta também.

No caso de reprovação por falta, se a nota final > nota configurada para reprovação por falta, a nota deveria ser alterada e isso deveria ficar evidente na tela sugerida no comentário a seguir.
Se for menor ou igual, mantenha a nota digitada.

class_enrollment.situation = ClassEnrollment::APPROVED
else
class_enrollment.situation = ClassEnrollment::DISAPPROVED
end
class_enrollment.save
end
end
flash[:info] = "Notas importadas com sucesso!"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Olhando o código, não vejo nada errado, mas a funcionalidade de upload de xlsx em turma não funcionou para mim. Aparece a mensagem dizendo que foi carregado, mas as notas não alteraram.

Além disso, poderia ser interessante ter uma tela mostrando as alterações que foram feitas com o upload. Ajudaria tanto nesse debug quanto como feedback pro professor saber que funcionou

return redirect_to course_classes_path
end
respond_to do |format|
format.html { render layout: false if request.xhr? }
end
end

protected
def before_update_save(record)
return unless
Expand Down
35 changes: 10 additions & 25 deletions app/helpers/concerns/class_enrollment_helper_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,36 +31,21 @@ def custom_disapproved_by_absence_form_column(record, options)
CustomVariable.grade_of_disapproval_for_absence.nil? ?
nil : CustomVariable.grade_of_disapproval_for_absence.to_f / 10.0
}",
course_has_grade: "#{record.course_has_grade}"
course_has_grade: "#{record.course_has_grade}",
overwrite_confirm_msg: I18n.t("activerecord.attributes.class_enrollment.confirm_grade_overwrite"),
data_disapproved: ClassEnrollment::DISAPPROVED
})
if record.course_has_grade
grade_of_disapproval_for_absence = (
(!CustomVariable.grade_of_disapproval_for_absence) ||
(CustomVariable.grade_of_disapproval_for_absence.nil?)
) ? nil : CustomVariable.grade_of_disapproval_for_absence.to_f / 10.0
options = options.merge(
onchange: "
if(
(this.checked) &&
(document.getElementById('record_grade_#{
record.course_class_id
}_class_enrollments_#{record.id}').value.trim() == '')
){
document.getElementById('record_grade_#{
record.course_class_id
}_class_enrollments_#{record.id}').value = '#{
grade_of_disapproval_for_absence
}';
}
"
)
end
check_box(:record, :disapproved_by_absence_to_view, options)
end

def custom_grade_form_column(record, options)
return "" if !record.course_has_grade
options = options.merge({
maxlength: 5, class: "grade-input numeric-input text-input"
maxlength: 5, class: "grade-input numeric-input text-input",
data_approved: ClassEnrollment::APPROVED,
data_disapproved: ClassEnrollment::DISAPPROVED,
grade_placeholder: I18n.t("activerecord.attributes.class_enrollment.placeholder_grade"),
minimum_grade_for_approval: (CustomVariable.minimum_grade_for_approval.to_f / 10.0)
})
text_field(:record, :grade_to_view, options)
end
Expand Down Expand Up @@ -121,6 +106,6 @@ def custom_field_attributes(column, record)
if can?(:post_grades, record) && cannot?(:update_all_fields, record)
return { style: "display:none;" }
end
return { style: "width: 190px;" } if controller_name != "class_enrollments"
{ style: "width: 190px;" } if controller_name != "class_enrollments"
end
end
10 changes: 10 additions & 0 deletions app/views/course_classes/import_grades_xls.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<%= form_tag import_grades_xls_course_class_path(@course_class), multipart: true, class: "as_form update", "data-loading": true do %>
<h4 style="text-align: center;"> <%= I18n.t("xls_content.course_class.import_grades_xls_title", course: @course_class.label_with_course) %></h4>
<div style="padding-top: 0.3em;display: flex; align-items: center; justify-content: center; width: 100%; gap: 20px;">
<div style="margin: 0 auto; display: flex; align-items: center; gap: 10px;">
<label style="font-weight: bold; font-size: 0.9em; padding-right: 5px;"><%= I18n.t("xls_content.course_class.spreadsheet_label") %>:</label>
<%= file_field_tag :spreadsheet, required: true %>
<%= submit_tag I18n.t("xls_content.course_class.upload_button"), class: "submit",style: "margin: 0;" ,data: {disable_with: I18n.t("xls_content.course_class.upload_loading")} %>
</div>
</div>
<% end %>
3 changes: 3 additions & 0 deletions config/locales/class_enrollment.pt-BR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ pt-BR:
scholarship_durations_active: "Possui bolsa?"
advisor: "Orientador"
has_advisor: "Possui orientador?"
placeholder_grade: "_,_"
confirm_grade_overwrite: "Já existe uma nota digitada (actual_grade). Deseja substituí-la com a nota de reprovação por falta (grade_for_disapproval)?"


errors:
models:
Expand Down
7 changes: 7 additions & 0 deletions config/locales/course_class.pt-BR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ pt-BR:

xls_content:
course_class:
spreadsheet_label: "Planilha (.xlsx)"
upload_button: "Fazer Upload"
upload_loading: "Enviando..."
import_grades_xls_label: "Importar Notas"
import_grades_xls_title: "Importar notas para %{course}"
import_grades_xls_success: "Notas importadas com sucesso!"
import_grades_xls_error: "Erro ao importar planilha."
summary:
attendance: "Freq S/I"
student_email: "Email"
Expand Down
3 changes: 2 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
member do
get "summary_pdf"
get "summary_xls"
match "import_grades_xls", via: [:get, :post]
end
collection do
get "class_schedule_pdf"
Expand Down Expand Up @@ -157,7 +158,7 @@
record_select_routes
end



resources :scholarship_durations do
concerns :active_scaffold
Expand Down
39 changes: 39 additions & 0 deletions spec/controllers/concerns/shared_xls_concern_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright (c) Universidade Federal Fluminense (UFF).
# This file is part of SAPOS. Please, consult the license terms in the LICENSE file.

require "spec_helper"

RSpec.describe SharedXlsConcern, type: :concern do
include SharedXlsConcern

describe "parse_grades_xlsx" do
let(:enrollment1) { FactoryBot.build(:enrollment, enrollment_number: "101") }
let(:enrollment2) { FactoryBot.build(:enrollment, enrollment_number: "102") }
let(:class_enrollment1) { FactoryBot.build(:class_enrollment, enrollment: enrollment1, grade: 87) }
let(:class_enrollment2) { FactoryBot.build(:class_enrollment, enrollment: enrollment2, grade: nil) }

let(:xlsx_file) do
tempfile = Tempfile.new(["test", ".xlsx"])
tempfile.binmode
tempfile.write(render_course_classes_summary_xls([class_enrollment1, class_enrollment2]))
tempfile.rewind
ActionDispatch::Http::UploadedFile.new(tempfile: tempfile,filename: "test.xlsx")
end

it "returns a hash with enrollment numbers as keys" do
result = parse_grades_xlsx(xlsx_file)
expect(result).to be_a(Hash)
expect(result.keys).to include("101", "102")
end

it "returns the grade value for filled cells" do
result = parse_grades_xlsx(xlsx_file)
expect(result["101"]).to eq("8.7")
end

it "returns nil for empty grade cells" do
result = parse_grades_xlsx(xlsx_file)
expect(result["102"]).to be_nil
end
end
end
4 changes: 2 additions & 2 deletions spec/features/class_enrollments_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@
it "should have a disapproved by absence widget" do
page.send_keys :escape
find(:css, "#record_disapproved_by_absence_").set(true)
expect(page).to have_field("Nota", with: "1")
expect(page).to have_field("Nota", with: "1,0")
end

it "should have a justification_grade_not_count_in_gpr without a label" do
Expand Down Expand Up @@ -171,7 +171,7 @@
it "should be able to edit student" do
page.driver.browser.action.send_keys(:escape).perform
within(".as_form") do
fill_in "Nota", with: "6"
fill_in "Nota", with: "60"
end
click_button_and_wait "Atualizar"
expect(page).to have_css("td.grade_label-column", text: "6.0")
Expand Down
Loading