-
Notifications
You must be signed in to change notification settings - Fork 16
Issue 587 #607
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Issue 587 #607
Changes from all commits
1838233
2e3a23f
4ddc4d5
b9e29b6
4b3fe28
c8334dc
8601d53
2d04383
21d35c7
d56e26b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
| 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){ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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('.',','); | ||
| }); | ||
| } | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,11 @@ class CourseClassesController < ApplicationController | |
| page: true, | ||
| type: :member, | ||
| parameters: { format: :xlsx } | ||
| config.action_links.add "import_grades_xls", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" } | ||
|
|
@@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| class_enrollment.situation = ClassEnrollment::APPROVED | ||
| else | ||
| class_enrollment.situation = ClassEnrollment::DISAPPROVED | ||
| end | ||
| class_enrollment.save | ||
| end | ||
| end | ||
| flash[:info] = "Notas importadas com sucesso!" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
| 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 %> |
| 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 |
There was a problem hiding this comment.
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