diff --git a/app/assets/stylesheets/partials/_assignments.scss b/app/assets/stylesheets/partials/_assignments.scss index cd8e7ad01..b707de4b9 100644 --- a/app/assets/stylesheets/partials/_assignments.scss +++ b/app/assets/stylesheets/partials/_assignments.scss @@ -100,6 +100,10 @@ font-weight: bold; } +.assignment_sent_late { + color: #dca727; + background: transparent; +} #basic_info { .schedule_dates, .schedule_dates.hour { @@ -127,4 +131,10 @@ } } } -} \ No newline at end of file + + .input_late_submissions { + display: flex; + align-items: center; + gap: 4px; + } +} diff --git a/app/assets/stylesheets/partials/_scores.scss b/app/assets/stylesheets/partials/_scores.scss index e5411b6fa..b8396914d 100644 --- a/app/assets/stylesheets/partials/_scores.scss +++ b/app/assets/stylesheets/partials/_scores.scss @@ -160,6 +160,14 @@ tr#activities { color: $sent_color_link; } + .sent_late{ + color: $color_contrast_text; + } + + .sent_late a{ + color: $color_contrast_text; + } + .not_sent, .failed, .failed_working_hours { @@ -198,6 +206,7 @@ tr#activities { .not_sent, .not_started, .to_send, + .sent_late, .without_group { white-space: nowrap; } diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb index 8264d587a..f15cd203e 100644 --- a/app/controllers/assignments_controller.rb +++ b/app/controllers/assignments_controller.rb @@ -203,7 +203,7 @@ def participants private def assignment_params - params.require(:assignment).permit(:name, :enunciation, :type_assignment, :start_hour, :end_hour, :controlled, + params.require(:assignment).permit(:name, :enunciation, :type_assignment, :start_hour, :end_hour, :controlled, :allow_late_submissions, schedule_attributes: [:id, :start_date, :end_date], enunciation_files_attributes: [:id, :attachment, :_destroy], ip_reals_attributes: [:id, :ip_v4, :ip_v6, :_destroy]) diff --git a/app/helpers/reports_helper.rb b/app/helpers/reports_helper.rb index 91c0267dd..45c065383 100644 --- a/app/helpers/reports_helper.rb +++ b/app/helpers/reports_helper.rb @@ -461,6 +461,7 @@ def self.style(pdf, table, light_numbers = false) cell.text_color = '2900C2' if cell.content == I18n.t("scores.index.sent") cell.text_color = 'E12227' if cell.content == I18n.t("scores.index.not_sent") cell.text_color = '003E7A' if cell.content == I18n.t("scores.index.to_send") + cell.text_color = 'DCA727' if cell.content == I18n.t("scores.index.sent_late") cell.text_color = 'B15759' if cell.content == I18n.t("scores.index.without_group") end @@ -481,6 +482,7 @@ def self.style(pdf, table, light_numbers = false) cell.text_color = '2900C2' if cell.content == I18n.t('scores.situation.sent') cell.text_color = 'E12227' if cell.content == I18n.t('scores.situation.not_sent') cell.text_color = '003E7A' if cell.content == I18n.t('scores.situation.to_be_sent') + cell.text_color = 'DCA727' if cell.content == I18n.t('scores.situation.sent_late') cell.text_color = 'B15759' if cell.content == I18n.t('scores.situation.without_group') cell.text_color = '006666' if cell.content == I18n.t('scores.situation.opened') end @@ -567,8 +569,12 @@ def self.legends(pdf) pdf.fill_color = '666666' pdf.text "#{I18n.t("scores.index.not_started")} #{I18n.t("scores.index.not_started_complete")}" pdf.move_down 5 + pdf.fill_color = 'DCA727' + pdf.text "#{I18n.t("scores.index.sent_late")} #{I18n.t("scores.index.sent_late_complete")}" + pdf.move_down 5 pdf.fill_color = '000000' pdf.text I18n.t("scores.index.new_after_evaluation") + end def self.line_itens_summary(pdf, wh, users, merged_group=false) diff --git a/app/models/academic_allocation_user.rb b/app/models/academic_allocation_user.rb index cf13eaa57..43bbe6f02 100644 --- a/app/models/academic_allocation_user.rb +++ b/app/models/academic_allocation_user.rb @@ -352,7 +352,7 @@ def info has_files = !files.first.max.nil? - { grade: grade, working_hours: working_hours, comments: comments, has_files: has_files, file_sent_date: (has_files ? I18n.l(files.first.max.to_datetime, format: :normal) : ' - ') } + { grade: grade, working_hours: working_hours, comments: comments, has_files: has_files, file_sent_date: (has_files ? files.first.max.to_datetime : nil) } end end diff --git a/app/models/allocation_tag.rb b/app/models/allocation_tag.rb index 86a691209..966c99c2a 100644 --- a/app/models/allocation_tag.rb +++ b/app/models/allocation_tag.rb @@ -212,7 +212,7 @@ def self.get_by_params(params, related = false, lower_related = false) offer_id = rts.map(&:offer_id).first if offer opt = (lower_related ? { lower: true } : ((related || selected.nil?) ? {} : { name: selected.downcase })) - [rts.map{|rt| rt.at_ids(opt)}.uniq, selected, offer_id] + [rts.map{|rt| rt.at_ids(opt)}.uniq, selected, offer_id] end end diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 7aa285a8f..0de04657b 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -94,15 +94,16 @@ def info(user_id, allocation_tag_id, group_id = nil) info = academic_allocation.academic_allocation_users.where(params).first.try(:info) || { has_files: false, file_sent_date: ' - ' } comments = (!info[:comments].nil? && info[:comments].any?) - { situation: situation(info[:has_files], !group_id.nil?, info[:grade], info[:working_hours], comments), has_comments: comments, group_id: group_id }.merge(info) + { situation: situation(info[:has_files], !group_id.nil?, info[:grade], info[:working_hours], comments, info[:file_sent_date]), has_comments: comments, group_id: group_id }.merge(info) end - def situation(has_files, has_group, grade = nil, working_hours = nil, has_comments=[]) + def situation(has_files, has_group, grade = nil, working_hours = nil, has_comments=[], file_sent_date=nil) case when !started? then 'not_started' when (self.type_assignment == Assignment_Type_Group && !has_group) then 'without_group' when !grade.blank? || !working_hours.blank? || has_comments then 'corrected' + when has_files && is_late(file_sent_date) then 'sent_late' when has_files then 'sent' when on_going? then 'to_be_sent' when ended? then 'not_sent' @@ -111,6 +112,19 @@ def situation(has_files, has_group, grade = nil, working_hours = nil, has_commen end end + def is_late(file_sent_date) + return false if file_sent_date.blank? || file_sent_date == ' - ' + + has_hours = (!start_hour.blank? && !end_hour.blank?) + endt = has_hours ? (schedule.end_date.beginning_of_day + end_hour.split(':')[0].to_i.hours + end_hour.split(':')[1].to_i.minutes) : schedule.end_date.end_of_day + + file_sent_date > endt + end + + def is_evaluated?(aau_user_id, aau_academic_allocation_id) + AcademicAllocationUser.find_by(user_id: aau_user_id, academic_allocation_id: aau_academic_allocation_id).grade.present? + end + def students_without_groups(allocation_tag_id) User.joins(allocations: [:profile, allocation_tag: :academic_allocations]) .where("cast( profiles.types & '#{Profile_Type_Student}' as boolean )") @@ -143,6 +157,7 @@ def self.list_assigment(user_id, at_id, evaluative=false, frequency=false) when (current_date < schedules.start_date AND (assignments.start_hour IS NULL OR assignments.start_hour = '')) OR (current_date <= schedules.start_date AND (assignments.start_hour IS NOT NULL AND assignments.end_hour != '' AND current_time schedules.end_date) && (attachment_updated_at IS NOT NULL) then 'sent_late' when attachment_updated_at IS NOT NULL OR (is_recorded AND (initial_time + (interval '1 mins')*duration) < now()) then 'sent' when (current_date <= schedules.end_date AND (assignments.end_hour IS NULL OR assignments.end_hour = '')) OR (current_date <= schedules.end_date AND (assignments.end_hour IS NOT NULL AND assignments.end_hour != '' AND current_time<=to_timestamp(assignments.end_hour, 'HH24:MI:SS')::time)) then 'to_be_sent' when schedules.end_date < current_date then 'not_sent' diff --git a/app/models/assignment_file.rb b/app/models/assignment_file.rb index 3e68cdd0b..dd2ded814 100644 --- a/app/models/assignment_file.rb +++ b/app/models/assignment_file.rb @@ -32,12 +32,15 @@ def order end def can_change? + # Allow when within time or when assignment explicitly allows late submissions + raise 'assignment_is_evaluated' if assignment.is_evaluated?(user_id, academic_allocation_user.academic_allocation_id) + return true if assignment.try(:allow_late_submissions) raise 'date_range_expired' unless assignment.in_time? end def can_destroy? raise CanCan::AccessDenied unless user_id == User.current.try(:id) - raise 'date_range_expired' unless assignment.in_time? + raise 'assignment_is_evaluated' if assignment.is_evaluated?(user_id, academic_allocation_user.academic_allocation_id) end def delete_with_dependents diff --git a/app/models/score.rb b/app/models/score.rb index 8df285b04..ff4281fc2 100644 --- a/app/models/score.rb +++ b/app/models/score.rb @@ -79,7 +79,8 @@ def self.evaluative_frequency(ats, type_score='evaluative') CASE #{evaluated_status} WHEN assignments.id IS NOT NULL AND assignments.type_assignment = #{Assignment_Type_Group} AND gp.id IS NULL THEN 'without_group' - WHEN (current_date < schedules.start_date AND (assignments.start_hour IS NULL OR assignments.start_hour = '')) OR (current_date = schedules.start_date AND (assignments.start_hour IS NOT NULL AND assignments.start_hour != '' AND current_time schedules.end_date + COALESCE(to_timestamp(NULLIF(assignments.end_hour, ''), 'HH24:MI')::time, '23:59'::time) THEN 'sent_late' WHEN (#{sent_status} OR ((academic_allocation_users.status IS NULL OR academic_allocation_users.status = 2) AND (academic_allocations.academic_tool_type = 'Assignment' AND (af.id IS NOT NULL OR (aw.id IS NOT NULL))))) THEN 'sent' WHEN schedules.end_date >= current_date THEN 'to_send' ELSE @@ -552,9 +553,10 @@ def self.get_query(tools, ats, user_id, wq, sent_status='', evaluated_status='', false END AS closed, case - #{evaluated_status} + #{evaluated_status} when assignments.type_assignment = #{Assignment_Type_Group} AND groups.group_id IS NULL then 'without_group' when (current_date < schedules.start_date AND (assignments.start_hour IS NULL OR assignments.start_hour = '')) OR (current_date = schedules.start_date AND (assignments.start_hour IS NOT NULL AND assignments.start_hour != '' AND current_time schedules.end_date + COALESCE(to_timestamp(NULLIF(assignments.end_hour, ''), 'HH24:MI')::time, '23:59'::time)) then 'sent_late' when #{sent_status} OR academic_allocation_users.status = 1 OR attachment_updated_at IS NOT NULL OR (assignment_webconferences.id IS NOT NULL AND is_recorded AND (initial_time + (interval '1 mins')*duration) < now()) then 'sent' when (current_date <= schedules.end_date AND current_date >= schedules.start_date AND (assignments.end_hour IS NULL OR assignments.end_hour = '' AND assignments.start_hour IS NULL OR assignments.start_hour = '')) OR (current_date <= schedules.end_date AND current_date >= schedules.start_date AND (assignments.end_hour IS NOT NULL AND assignments.end_hour != '' AND current_time<=to_timestamp(assignments.end_hour, 'HH24:MI:SS')::time)) then 'to_be_sent' else 'not_sent' diff --git a/app/views/assignment_files/_list.html.haml b/app/views/assignment_files/_list.html.haml index 51d0eb733..06a78fd05 100755 --- a/app/views/assignment_files/_list.html.haml +++ b/app/views/assignment_files/_list.html.haml @@ -4,12 +4,12 @@ %i.icon-suitcase - title =(@assignment.type_assignment == Assignment_Type_Group ? t('.title_group') : t('.title_student')) = title - - if right_actions - .right_buttons - - unless files.blank? - = link_to content_tag(:i, nil, class: 'icon-install'), zip_download_assignment_files_path(assignment_id: @assignment.id, student_id: @student_id, group_id: @group_id), class: 'btn', :"data-tooltip" => t('.download'), :'aria-label' => t(".download")+ title - - if @own_assignment && !disabled - = link_to content_tag(:i, nil, class: 'icon-plus'), "#void", onclick: "add_file(this); return false", class: "btn btn_main #{@in_time ? '' : 'disabled'}", :"data-tooltip" => t('.send'), :'aria-label' => t(".send") + t('fancybox.open'), onkeydown: 'click_on_keypress(event, this);', id: 'add_file', :'data-shortcut' => t("shortcut.assignment.code.file"), :'data-shortcut-name' => t("shortcut.assignment.name.file") + .right_buttons + - unless files.blank? + = link_to content_tag(:i, nil, class: 'icon-install'), zip_download_assignment_files_path(assignment_id: @assignment.id, student_id: @student_id, group_id: @group_id), class: 'btn', :"data-tooltip" => t('.download'), :'aria-label' => t(".download")+ title + - if @own_assignment && !disabled + - enabled = (@in_time || @assignment.try(:allow_late_submissions)) + = link_to content_tag(:i, nil, class: 'icon-plus'), "#void", onclick: "add_file(this); return false", class: "btn btn_main #{enabled ? '' : 'disabled'}", :"data-tooltip" => t('.send'), :'aria-label' => t(".send") + t('fancybox.open'), onkeydown: 'click_on_keypress(event, this);', id: 'add_file', :'data-shortcut' => t("shortcut.assignment.code.file"), :'data-shortcut-name' => t("shortcut.assignment.name.file") .block_content - no_files = (files.blank?) diff --git a/app/views/assignments/_assignments.html.haml b/app/views/assignments/_assignments.html.haml index f20acca8f..d975be797 100755 --- a/app/views/assignments/_assignments.html.haml +++ b/app/views/assignments/_assignments.html.haml @@ -79,4 +79,5 @@ var url = $(this).data("url"); $(this).call_fancybox({href: url, open: true}); }); - }); + } + ); diff --git a/app/views/assignments/_form.html.haml b/app/views/assignments/_form.html.haml index 5749dc7f7..a5ba8aed5 100644 --- a/app/views/assignments/_form.html.haml +++ b/app/views/assignments/_form.html.haml @@ -55,16 +55,23 @@ %span#file{:"data-file-id" => ef.id} = ef.attachment_file_name %i.icon-cross-square.warning.remove_file - - if @files_errors + - if @files_errors %span.field_with_errors.error= @files_errors - .input + .input_controlled = f.label :controlled = f.input :controlled, as: :boolean, label: false = link_to (image_tag "#{@assignment.controlled ? 'released' : 'rejected'}.png"), "#void", onclick: 'change(this, ["#with_control", "#without_control"])', :'data-tooltip' => (@assignment.controlled ? t('.environment_controlled') : t('.environment_not_controlled')), :'data-id' => 'controlled', :'data-active' => t('.environment_controlled'), :'data-not-active' => t('.environment_not_controlled') - if @assignment.errors[:controlled].any? %span.field_with_errors.error= @assignment.errors[:controlled].first + .input_late_submissions + = f.label :allow_late_submissions + = f.input :allow_late_submissions, as: :boolean, label: false + = link_to (image_tag "#{@assignment.allow_late_submissions ? 'released' : 'rejected'}.png"), "#void", onclick: 'change(this, ["#with_late", "#without_late"])', :'data-tooltip' => (@assignment.allow_late_submissions ? t('.late_submissions_allowed') : t('.late_submissions_not_allowed')), :'data-id' => 'allow_late_submissions', :'data-active' => t('.late_submissions_allowed'), :'data-not-active' => t('.late_submissions_not_allowed') + - if @assignment.errors[:allow_late_submissions].any? + %span.field_with_errors.error= @assignment.errors[:allow_late_submissions].first + = render "groups/codes" .right_buttons.clear#without_control diff --git a/app/views/assignments/_groups.html.haml b/app/views/assignments/_groups.html.haml index c463cf39c..8e3a356f0 100644 --- a/app/views/assignments/_groups.html.haml +++ b/app/views/assignments/_groups.html.haml @@ -17,7 +17,7 @@ - info = assignment.info(nil, @allocation_tag_id, group.id) %tr %td{headers: "group_#{assignment.id}" }= link_to group.group_name, student_assignment_path(assignment.id, group_id: group.id), class: :link_content - %td.center{headers: "sent_on_#{assignment.id}" }= info[:file_sent_date] + %td.center{headers: "sent_on_#{assignment.id}" }= info[:file_sent_date].is_a?(DateTime) ? l(info[:file_sent_date], format: :normal) : info[:file_sent_date] %td.center{class: "assignment_#{info[:situation]}", headers: "situation_#{assignment.id}"}= t(info[:situation].to_sym) %td.center{headers: "grade_#{assignment.id}" }= info[:grade] || "-" %td.center{headers: "working_hours_#{assignment.id}" }= info[:working_hours] || "-" diff --git a/app/views/assignments/_participants.html.haml b/app/views/assignments/_participants.html.haml index e1d37ac80..4a50c32d1 100644 --- a/app/views/assignments/_participants.html.haml +++ b/app/views/assignments/_participants.html.haml @@ -16,7 +16,7 @@ - info = assignment.info(participant.id, @allocation_tag_id) %tr %td{headers: "student_or_group_#{assignment.id}"}= link_to participant.name, student_assignment_path(assignment.id, student_id: participant.id), class: :link_content - %td.center{headers: "sent_on_#{assignment.id}"}= info[:file_sent_date] + %td.center{headers: "sent_on_#{assignment.id}"}= info[:file_sent_date].is_a?(DateTime) ? l(info[:file_sent_date], format: :normal) : info[:file_sent_date] %td.center{class: "assignment_#{info[:situation]}", headers: "situation_#{assignment.id}"}= t(info[:situation].to_sym) %td.center{headers: "grade_#{assignment.id}"}= info[:grade] || "-" %td.center{headers: "working_hours_#{assignment.id}"}= info[:working_hours] || "-" diff --git a/app/views/scores/index.html.haml b/app/views/scores/index.html.haml index 02b5fd080..0e07362fa 100755 --- a/app/views/scores/index.html.haml +++ b/app/views/scores/index.html.haml @@ -85,7 +85,10 @@ %li.not_started %abbr{title: t(".not_started_complete")}= t(".not_started") = t(".not_started_complete") + %li.sent_late + %abbr{title: t(".sent_late_complete")}= t(".sent_late") + = t(".sent_late_complete") %li.new_after_evaluation = t(".new_after_evaluation") - + = javascript_include_tag 'scores' diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index 52a5db416..e3dc08eef 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -368,6 +368,7 @@ en_US: start_hour: Schedule end_hour: to controlled: Controlled + allow_late_submissions: "Allow Late Submission" assignment_enunciation_file: attachment_file_size: "File too large. The maximum size should be up to 5MB." attachment_file_name: File @@ -1637,6 +1638,7 @@ en_US: not_sent: Not Sent to_be_sent: To be sent to_send: To be sent + sent_late: Sent Late without_group: Without Group comment_error_delete_file: Error when trying to delete file group: "Group" @@ -2019,6 +2021,8 @@ en_US: end_hour: to environment_controlled: 'You can control where the student will take the assignment (for example: labs, academic blocks, etc.)' environment_not_controlled: 'The student can take the assignment in any place (for example: laboratories, home, lan houses, etc.)' + late_submissions_allowed: "Allows the student to submit the assignment after its due" + late_submissions_not_allowed: "Does not allow the student to submit the assignment after its due" continue: Continue previous: Previous control: @@ -2177,6 +2181,7 @@ en_US: not_started_up: It was not possible to send file. Assignment not started yet. note_update: It was not possible to update file's note. cannot_save_empty: Cannot save empty note. + assignment_is_evaluated: It was not possible to execute action. Assignment is already evaluated. success: removed: File removed successfully uploaded: File uploaded successfully @@ -3948,6 +3953,8 @@ en_US: error_msg_register_notes: Error releasing / blocking note log msg_button_register_off: Record of notes blocked by the person in charge error_msg_register_notes_active_open: "Error: There are unfinished activities." + sent_late: AD + sent_late_complete: "Activity sent after the deadline" info: assignments: Assignments date_range: Date range diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index d61617491..e3029445b 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -432,6 +432,7 @@ pt_BR: start_hour: Horário end_hour: à controlled: "Controlado" + allow_late_submissions: "Permitir Atraso" assignment_enunciation_file: attachment_content_type: "" attachment_file_size: "Arquivo muito grande. O tamanho máximo deve ser de até 5MB." @@ -1906,6 +1907,7 @@ pt_BR: not_sent: Não Enviado to_be_sent: A enviar to_send: A enviar + sent_late: "Enviado em Atraso" without_group: Sem Grupo not_corrected: Não Corrigido confirm_create_group_assignment_in_today: "O trabalho está iniciado hoje, por esse motivo os alunos serão alocados em grupos automaticamente." @@ -2305,6 +2307,8 @@ pt_BR: end_hour: à environment_controlled: 'Você pode controlar o local de onde o aluno irá realizar o trabalho (por exemplo: laboratórios, blocos, etc)' environment_not_controlled: 'O aluno poderá realizar este trabalho em qualquer local (por exemplo: laboratórios, casa, lan houses, etc)' + late_submissions_allowed: "Permite que o aluno envie o trabalho após o término do período" + late_submissions_not_allowed: "Não permite que o aluno envie o trabalho após o término do período" continue: Continuar previous: Voltar control: @@ -2463,6 +2467,7 @@ pt_BR: not_started_up: Não é possível fazer o envio do arquivo. O trabalho ainda não está iniciado. note_update: Não foi possível atualizar comentário do arquivo. cannot_save_empty: Não é possível salvar comentário vazio. + assignment_is_evaluated: Não é possível realizar ação. Trabalho já foi avaliado. success: removed: Arquivo deletado com sucesso uploaded: Arquivo enviado com sucesso @@ -4265,6 +4270,8 @@ pt_BR: error_msg_register_notes: Erro ao liberar/bloquear registro de notas msg_button_register_off: Registro de notas bloqueada pelo responsável error_msg_register_notes_active_open: "Erro: Existem atividades não finalizadas." + sent_late: AT + sent_late_complete: Atividade enviada fora do prazo info: assignments: Trabalhos date_range: Período @@ -4384,6 +4391,7 @@ pt_BR: started: Iniciado sent: Enviado not_sent: Não Enviado + sent_late: Enviado em Atraso to_be_sent: A enviar without_group: Sem Grupo opened: Iniciado diff --git a/db/migrate/20251202000000_add_allow_late_submissions_to_assignments.rb b/db/migrate/20251202000000_add_allow_late_submissions_to_assignments.rb new file mode 100644 index 000000000..4d5697e50 --- /dev/null +++ b/db/migrate/20251202000000_add_allow_late_submissions_to_assignments.rb @@ -0,0 +1,5 @@ +class AddAllowLateSubmissionsToAssignments < ActiveRecord::Migration[5.1] + def change + add_column :assignments, :allow_late_submissions, :boolean, default: false, null: false + end +end